smSnotification.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. <template>
  2. <el-dialog
  3. v-model="dialogVisible"
  4. :title="getDialogTitle"
  5. width="60%"
  6. :close-on-click-modal="false"
  7. @closed="handleClosed"
  8. custom-class="interview-notification-dialog"
  9. >
  10. <div class="notification">
  11. <div class="notification-container">
  12. <!-- 消息头部 -->
  13. <div class="message-header">
  14. <div class="avatar">
  15. <img src="/src/assets/qcLogo.png" alt="公司头像" />
  16. </div>
  17. <div class="contact-info">
  18. <span class="phone">+10688028...</span>
  19. <div class="contact-actions">
  20. <el-button link type="primary" size="small">
  21. <el-icon><Phone /></el-icon>
  22. </el-button>
  23. <el-button link type="primary" size="small">
  24. <el-icon><MoreFilled /></el-icon>
  25. </el-button>
  26. </div>
  27. </div>
  28. </div>
  29. <!-- 消息内容 -->
  30. <div class="message-content">
  31. <!-- 根据当前模板显示内容 -->
  32. <div v-if="currentTemplate" v-html="currentTemplate.template"></div>
  33. <!-- 当没有模板时显示的默认内容 -->
  34. <div v-else class="no-template">
  35. 暂无可用的短信模板
  36. </div>
  37. </div>
  38. <div class="notification-right-footer-action">
  39. <el-icon><Plus /></el-icon>
  40. <div class="input"></div>
  41. <el-icon><Plus /></el-icon>
  42. <el-icon><Upload /></el-icon>
  43. </div>
  44. </div>
  45. <!-- 右侧预览区域 -->
  46. <div class="notification-right">
  47. <div class="notification-right-header">
  48. <div v-if="currentTemplate" class="template-info">
  49. <h3>模板信息</h3>
  50. <p>模板ID: {{ currentTemplate.id }}</p>
  51. <p>模板类型: {{ getTemplateTypeName }}</p>
  52. </div>
  53. </div>
  54. <div class="notification-right-content">
  55. <div v-if="currentTemplate" v-html="currentTemplate.template"></div>
  56. </div>
  57. </div>
  58. </div>
  59. <template #footer>
  60. <div class="dialog-footer">
  61. <el-button @click="dialogVisible = false">关闭</el-button>
  62. <el-button type="primary" @click="handleSend" :loading="loading">发送通知</el-button>
  63. </div>
  64. </template>
  65. </el-dialog>
  66. <!-- 编辑表单对话框 -->
  67. <el-dialog
  68. v-model="editDialogVisible"
  69. title="编辑面试邀请"
  70. width="600px"
  71. :close-on-click-modal="false"
  72. >
  73. <el-form :model="editForm" ref="editFormRef" :rules="editRules" label-width="100px">
  74. <el-form-item label="手机号" prop="phone">
  75. <el-input v-model="editForm.phone" placeholder="请输入手机号" />
  76. </el-form-item>
  77. <el-form-item label="候选人姓名" prop="candidateName">
  78. <el-input v-model="editForm.candidateName" placeholder="请输入候选人姓名" />
  79. </el-form-item>
  80. <el-form-item label="公司名称" prop="companyName">
  81. <el-input v-model="editForm.companyName" placeholder="请输入公司名称" />
  82. </el-form-item>
  83. <el-form-item label="面试职位" prop="position">
  84. <el-input v-model="editForm.position" placeholder="请输入面试职位" />
  85. </el-form-item>
  86. <el-form-item label="面试日期" prop="interviewDate">
  87. <el-date-picker
  88. v-model="editForm.interviewDate"
  89. type="date"
  90. placeholder="选择面试日期"
  91. format="YYYY-MM-DD"
  92. value-format="YYYY-MM-DD"
  93. />
  94. </el-form-item>
  95. <el-form-item label="面试时间" prop="interviewTime">
  96. <el-time-picker
  97. v-model="editForm.interviewTime"
  98. format="HH:mm"
  99. value-format="HH:mm"
  100. placeholder="选择面试时间"
  101. />
  102. </el-form-item>
  103. <el-form-item label="面试码" prop="interviewCode">
  104. <el-input v-model="editForm.interviewCode" placeholder="请输入面试码" />
  105. </el-form-item>
  106. <el-form-item label="面试方式" prop="interviewMethod">
  107. <el-select v-model="editForm.interviewMethod" placeholder="请选择面试方式">
  108. <el-option label="线上面试" value="online" />
  109. <el-option label="线下面试" value="offline" />
  110. </el-select>
  111. </el-form-item>
  112. <el-form-item label="面试链接" prop="interviewLink" v-if="editForm.interviewMethod === 'online'">
  113. <el-input v-model="editForm.interviewLink" placeholder="请输入面试链接" />
  114. </el-form-item>
  115. <el-form-item label="面试地址" prop="interviewAddress" v-if="editForm.interviewMethod === 'offline'">
  116. <el-input v-model="editForm.interviewAddress" type="textarea" placeholder="请输入面试地址" />
  117. </el-form-item>
  118. </el-form>
  119. <template #footer>
  120. <el-button @click="editDialogVisible = false">取消</el-button>
  121. <el-button type="primary" @click="handleSaveEdit">保存</el-button>
  122. </template>
  123. </el-dialog>
  124. </template>
  125. <script lang="ts" setup>
  126. import { ref, reactive, onMounted, computed } from 'vue';
  127. import { ElMessage, FormInstance } from 'element-plus';
  128. import { Phone, MoreFilled, Edit } from '@element-plus/icons-vue';
  129. import { updateBatchStatus } from '../api';
  130. import axios from 'axios';
  131. import { Session } from '/@/utils/storage';
  132. const dialogVisible = ref(false);
  133. const editDialogVisible = ref(false);
  134. const loading = ref(false);
  135. const formRef = ref<FormInstance>();
  136. const editFormRef = ref<FormInstance>();
  137. // 添加当前状态的响应式变量
  138. const currentStatus = ref(0);
  139. // 添加模板相关的响应式变量
  140. const templates = ref([]);
  141. const currentTemplate = ref(null);
  142. // 原有表单数据
  143. const form = reactive({
  144. new_status: undefined as number | undefined,
  145. note: '',
  146. application_ids: [] as number[]
  147. });
  148. // 编辑表单数据
  149. const editForm = reactive({
  150. phone: '', // 手机号
  151. candidateName: '', // 候选人姓名
  152. companyName: '', // 公司名称
  153. position: '', // 职位名称
  154. interviewDate: '', // 面试日期
  155. interviewTime: '', // 面试时间
  156. interviewCode: '', // 面试码
  157. interviewMethod: 'online',
  158. interviewLink: '',
  159. interviewAddress: ''
  160. });
  161. const rules = {
  162. new_status: [{ required: true, message: '请选择新状态', trigger: 'change' }]
  163. };
  164. const editRules = {
  165. phone: [
  166. { required: true, message: '请输入手机号', trigger: 'blur' },
  167. { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
  168. ],
  169. candidateName: [
  170. { required: true, message: '请输入候选人姓名', trigger: 'blur' }
  171. ],
  172. companyName: [
  173. { required: true, message: '请输入公司名称', trigger: 'blur' }
  174. ],
  175. position: [
  176. { required: true, message: '请输入职位名称', trigger: 'blur' }
  177. ],
  178. interviewDate: [
  179. { required: true, message: '请选择面试日期', trigger: 'change' }
  180. ],
  181. interviewTime: [
  182. { required: true, message: '请选择面试时间', trigger: 'change' }
  183. ],
  184. interviewCode: [
  185. { required: true, message: '请输入面试码', trigger: 'blur' }
  186. ],
  187. interviewMethod: [
  188. { required: true, message: '请选择面试方式', trigger: 'change' }
  189. ]
  190. };
  191. const emit = defineEmits(['success']);
  192. const props = defineProps({
  193. crudExpose: {
  194. type: Object,
  195. required: true
  196. }
  197. });
  198. // 获取短信模板
  199. const fetchSmsTemplates = async () => {
  200. try {
  201. const response = await axios.get(`${import.meta.env.VITE_API_URL}/api/system/sms/templates/`,
  202. {
  203. headers: {
  204. 'Authorization': `JWT ${Session.get('token')}`
  205. }
  206. }
  207. );
  208. if (response.data.code === 2000) {
  209. console.log(response.data)
  210. templates.value = response.data.data.templates;
  211. updateCurrentTemplate();
  212. } else {
  213. ElMessage.error('获取短信模板失败');
  214. }
  215. } catch (error) {
  216. console.error('获取短信模板失败:', error);
  217. ElMessage.error('获取短信模板失败');
  218. }
  219. };
  220. // 更新当前模板
  221. const updateCurrentTemplate = () => {
  222. if (!templates.value.length) return;
  223. let templateIndex;
  224. switch (currentStatus.value) {
  225. case 0:
  226. templateIndex = 1; // 对应序号1的模板
  227. break;
  228. case 3:
  229. templateIndex = 2; // 对应序号2的模板
  230. break;
  231. case 4:
  232. templateIndex = 3; // 对应序号3的模板
  233. break;
  234. default:
  235. templateIndex = 0;
  236. }
  237. currentTemplate.value = templates.value[templateIndex];
  238. };
  239. // 在组件挂载时获取模板
  240. onMounted(() => {
  241. fetchSmsTemplates();
  242. });
  243. // 修改open函数
  244. const open = async (selection: any[]) => {
  245. dialogVisible.value = true;
  246. form.application_ids = selection.map(item => item.id);
  247. // 设置当前状态(使用第一个选中项的状态)
  248. currentStatus.value = selection[0]?.status || 0;
  249. // 获取并更新模板
  250. await fetchSmsTemplates();
  251. // 初始化编辑表单数据,使用选中项的第一条数据
  252. const firstItem = selection[0];
  253. if (firstItem) {
  254. editForm.phone = firstItem.applicant_phone || ''; // 使用申请人电话
  255. editForm.candidateName = firstItem.applicant_name || ''; // 使用申请人姓名
  256. editForm.companyName = '上海敏龙智创科技发展有限公司'; // 固定公司名称
  257. editForm.position = firstItem.position_title || ''; // 使用职位名称
  258. editForm.interviewDate = ''; // 面试日期需要用户选择
  259. editForm.interviewTime = ''; // 面试时间需要用户选择
  260. editForm.interviewCode = generateInterviewCode(); // 生成面试码
  261. editForm.interviewMethod = 'online';
  262. editForm.interviewLink = '';
  263. editForm.interviewAddress = '';
  264. }
  265. };
  266. // 添加生成面试码的函数
  267. const generateInterviewCode = () => {
  268. const prefix = 'ML';
  269. const number = Math.floor(Math.random() * 999).toString().padStart(3, '0');
  270. return `${prefix}${number}`;
  271. };
  272. // 处理编辑
  273. const handleEdit = () => {
  274. editDialogVisible.value = true;
  275. };
  276. // 保存编辑
  277. const handleSaveEdit = async () => {
  278. if (!editFormRef.value) return;
  279. await editFormRef.value.validate((valid) => {
  280. if (valid) {
  281. ElMessage.success('编辑保存成功');
  282. editDialogVisible.value = false;
  283. }
  284. });
  285. };
  286. // 添加日期格式化函数
  287. const formatDate = (date: string) => {
  288. if (!date) return '';
  289. const d = new Date(date);
  290. return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`;
  291. };
  292. // 添加时间格式化函数
  293. const formatTime = (time: string) => {
  294. if (!time) return '';
  295. const [hours, minutes] = time.split(':');
  296. const hour = parseInt(hours);
  297. const period = hour >= 12 ? '下午' : '上午';
  298. const formattedHour = hour > 12 ? hour - 12 : hour;
  299. return `${period}${formattedHour}:${minutes}`;
  300. };
  301. // 根据状态获取对应的API接口
  302. const getApiEndpoint = (status: number) => {
  303. const baseUrl = import.meta.env.VITE_API_URL;
  304. switch (status) {
  305. case 0:
  306. return `${baseUrl}/api/system/sms/interview-notification/`;
  307. case 3:
  308. return `${baseUrl}/api/system/sms/interview-passed/`;
  309. case 4:
  310. return `${baseUrl}/api/system/sms/interview-failed/`;
  311. default:
  312. throw new Error('不支持的状态类型');
  313. }
  314. };
  315. // 根据状态获取对应的请求数据
  316. const getRequestData = (status: number) => {
  317. const baseData = {
  318. application_ids: form.application_ids,
  319. tenant_id: Session.get('tenant_id'),
  320. phone: editForm.phone,
  321. name: editForm.candidateName,
  322. company: editForm.companyName,
  323. job: editForm.position,
  324. date: formatDate(editForm.interviewDate) || '2025年7月30日',
  325. time: formatTime(editForm.interviewTime) || '10:00',
  326. code: editForm.interviewCode
  327. };
  328. switch (status) {
  329. case 0:
  330. return {
  331. ...baseData,
  332. interview_method: editForm.interviewMethod,
  333. interview_link: editForm.interviewLink,
  334. interview_address: editForm.interviewAddress
  335. };
  336. case 3:
  337. return {
  338. ...baseData,
  339. onboard_time: editForm.interviewTime,
  340. position: editForm.position
  341. };
  342. case 4:
  343. return {
  344. ...baseData,
  345. reason: form.note
  346. };
  347. default:
  348. return baseData;
  349. }
  350. };
  351. // 发送通知
  352. const handleSend = async () => {
  353. loading.value = true;
  354. try {
  355. // 获取对应状态的API接口
  356. const apiEndpoint = getApiEndpoint(currentStatus.value);
  357. // 获取对应状态的请求数据
  358. const requestData = getRequestData(currentStatus.value);
  359. // 发送请求
  360. const response = await axios.post(apiEndpoint, requestData,
  361. {
  362. headers: {
  363. 'Authorization': `JWT ${Session.get('token')}`
  364. }
  365. }
  366. );
  367. if (response.data.code === 2000) {
  368. ElMessage.success('通知发送成功');
  369. dialogVisible.value = false;
  370. emit('success');
  371. props.crudExpose.doRefresh();
  372. } else {
  373. ElMessage.error(response.data.msg || '发送失败');
  374. }
  375. } catch (error: any) {
  376. console.error('发送通知失败:', error);
  377. ElMessage.error(error.message || '发送失败,请重试');
  378. } finally {
  379. loading.value = false;
  380. }
  381. };
  382. // 提交表单(保留原有逻辑)
  383. const handleSubmit = async () => {
  384. if (!formRef.value) return;
  385. await formRef.value.validate(async (valid) => {
  386. if (!valid) return;
  387. loading.value = true;
  388. try {
  389. const data = {
  390. application_ids: form.application_ids,
  391. new_status: form.new_status as number,
  392. note: form.note,
  393. tenant_id:Session.get('tenant_id')
  394. };
  395. const res = await updateBatchStatus(data);
  396. if (res.code === 2000) {
  397. ElMessage.success('批量修改状态成功');
  398. dialogVisible.value = false;
  399. emit('success');
  400. props.crudExpose.doRefresh();
  401. } else {
  402. ElMessage.error(res.msg || '操作失败');
  403. }
  404. } catch (error) {
  405. console.error('批量修改状态失败:', error);
  406. ElMessage.error('操作失败,请重试');
  407. } finally {
  408. loading.value = false;
  409. }
  410. });
  411. };
  412. // 关闭对话框时重置表单
  413. const handleClosed = () => {
  414. if (formRef.value) {
  415. formRef.value.resetFields();
  416. }
  417. };
  418. // 暴露方法给父组件
  419. defineExpose({
  420. open
  421. });
  422. // 添加计算属性
  423. const getDialogTitle = computed(() => {
  424. switch (currentStatus.value) {
  425. case 0:
  426. return '面试邀请通知';
  427. case 2:
  428. return '面试通过通知';
  429. case 4:
  430. return '面试未通过通知';
  431. default:
  432. return '短信通知';
  433. }
  434. });
  435. const getTemplateTypeName = computed(() => {
  436. switch (currentStatus.value) {
  437. case 0:
  438. return '面试邀请模板';
  439. case 2:
  440. return '录用通知模板';
  441. case 4:
  442. return '婉拒通知模板';
  443. default:
  444. return '通用模板';
  445. }
  446. });
  447. </script>
  448. <style scoped>
  449. .notification {
  450. display: flex;
  451. }
  452. .notification-container {
  453. position: relative;
  454. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  455. background: #f7f7f7;
  456. border-radius: 8px;
  457. padding: 20px;
  458. min-height: 400px;
  459. width: 45%;
  460. border: 1px solid #e4e7ed;
  461. }
  462. .notification-right{
  463. width: 100%;
  464. background: #f7f7f7;
  465. font-size: 12px;
  466. color: #595959;
  467. border-radius: 8px;
  468. margin-left: 20px;
  469. padding: 20px;
  470. min-height: 400px;
  471. }
  472. .notification-right-header{
  473. margin: 30px 0;
  474. }
  475. .notification-right-content-title{
  476. color: #595959;
  477. font-weight: 600;
  478. }
  479. .notification-right-content{
  480. padding:0 10px;
  481. margin: 50px 0;
  482. line-height: 1.6;
  483. }
  484. .notification-right-footer{
  485. }
  486. .message-header {
  487. display: flex;
  488. align-items: center;
  489. padding: 5px 0;
  490. border-bottom: 1px solid #e4e7ed;
  491. margin-bottom: 15px;
  492. }
  493. .avatar {
  494. width: 30px;
  495. height: 30px;
  496. border-radius: 4px;
  497. overflow: hidden;
  498. margin-right: 10px;
  499. }
  500. .avatar img {
  501. width: 100%;
  502. height: 100%;
  503. object-fit: cover;
  504. }
  505. .contact-info {
  506. flex: 1;
  507. display: flex;
  508. justify-content: space-between;
  509. align-items: center;
  510. }
  511. .phone {
  512. font-size: 16px;
  513. font-weight: 500;
  514. }
  515. .contact-actions {
  516. display: flex;
  517. gap: 10px;
  518. }
  519. .message-content {
  520. background: #fff;
  521. border-radius: 8px;
  522. padding: 15px;
  523. margin-bottom: 55px;
  524. position: relative;
  525. /* margin-left: 50px; */
  526. box-shadow: 0 2px 6px rgba(0,0,0,0.1);
  527. }
  528. .notification-right-footer-action{
  529. position: absolute;
  530. bottom: -10px;
  531. left: 0;
  532. right: 0;
  533. display: flex;
  534. align-items: center;
  535. justify-content: space-between;
  536. margin-top: 10px;
  537. margin-bottom: 10px;
  538. padding: 10px;
  539. background: #fff;
  540. border-top-left-radius: 15px;
  541. border-top-right-radius: 15px;
  542. }
  543. .input{
  544. width: 70%;
  545. height: 20px;
  546. border-radius: 10px;
  547. background: #f2f2f2;
  548. }
  549. /* .message-content::before {
  550. content: '';
  551. position: absolute;
  552. left: -8px;
  553. top: 15px;
  554. width: 0;
  555. height: 0;
  556. border-top: 8px solid transparent;
  557. border-bottom: 8px solid transparent;
  558. } */
  559. .invitation-title {
  560. font-size: 12px;
  561. font-weight: 600;
  562. padding-left: 12px;
  563. margin-bottom: 10px;
  564. color: #333;
  565. }
  566. .invitation-content {
  567. font-size: 12px;
  568. line-height: 1.6;
  569. color: #666;
  570. }
  571. .invitation-content p {
  572. margin-bottom: 10px;
  573. }
  574. .highlight {
  575. color: #595959;
  576. font-weight: 600;
  577. }
  578. .company-name {
  579. color: #595959;
  580. font-weight: 500;
  581. }
  582. .interview-details {
  583. margin: 10px 0;
  584. padding: 10px;
  585. background: rgba(255,255,255,0.3);
  586. border-radius: 4px;
  587. border-left: 3px solid #fff;
  588. }
  589. .detail-item {
  590. margin-bottom: 8px;
  591. font-size: 12px;
  592. }
  593. .detail-item strong {
  594. color: #333;
  595. font-weight: 600;
  596. }
  597. .editable-field {
  598. color: #595959;
  599. background: #ecf5ff;
  600. padding: 2px 6px;
  601. border-radius: 3px;
  602. font-size: 12px;
  603. }
  604. .notice-section {
  605. margin: 10px 0;
  606. padding: 12px;
  607. background: rgba(255,255,255,0.2);
  608. border-radius: 4px;
  609. border: 1px solid rgba(255,255,255,0.4);
  610. }
  611. .notice-title {
  612. font-weight: 600;
  613. color: #595959;
  614. margin-bottom: 8px;
  615. }
  616. .notice-content {
  617. font-size: 13px;
  618. color: #666;
  619. }
  620. .notice-content p {
  621. margin-bottom: 5px;
  622. }
  623. .company-signature {
  624. margin-top: 15px;
  625. text-align: left;
  626. padding-left: 12px;
  627. font-size: 13px;
  628. color: #999;
  629. }
  630. .date {
  631. margin-top: 3px;
  632. }
  633. .edit-section {
  634. text-align: left;
  635. margin-top: 15px;
  636. padding-top: 10px;
  637. }
  638. .dialog-footer {
  639. text-align: right;
  640. }
  641. :deep(.interview-notification-dialog) {
  642. border-radius: 8px;
  643. }
  644. :deep(.interview-notification-dialog .el-dialog__header) {
  645. background: #f8f9fa;
  646. border-radius: 8px 8px 0 0;
  647. }
  648. :deep(.interview-notification-dialog .el-dialog__body) {
  649. padding: 20px;
  650. }
  651. .template-info {
  652. margin-bottom: 20px;
  653. padding: 10px;
  654. background: #f5f7fa;
  655. border-radius: 4px;
  656. }
  657. .template-info h3 {
  658. margin-bottom: 10px;
  659. color: #303133;
  660. font-size: 16px;
  661. }
  662. .template-info p {
  663. margin: 5px 0;
  664. color: #606266;
  665. font-size: 14px;
  666. }
  667. .no-template {
  668. text-align: center;
  669. padding: 20px;
  670. color: #909399;
  671. font-size: 14px;
  672. }
  673. </style>