| New file |
| | |
| | | <?php |
| | | /** |
| | | * WPCode çæ®µ 4/4 â åå°ç®¡ç + Nginx è·¯ç±æ³¨å |
| | | * å称建议ï¼IM - åå°ç®¡ç |
| | | * ç±»åï¼PHP Snippet |
| | | * ä½ç½®ï¼Run everywhere |
| | | * |
| | | * ä¾èµï¼ç段 1ã2ã3 å¿
é¡»å
å¯ç¨ |
| | | * |
| | | * å
å«ï¼ |
| | | * - åé人åè¡¨é¡µï¼æç´¢ / ç¶æçé / åé¡µï¼ |
| | | * - åé人详æ
页ï¼å®æ´è¡¨åä¿¡æ¯ / éä»¶ä¸è½½ / é¢è¯è®°å½ / è§é¢ææ¾ï¼ |
| | | * - AJAX åéé¢è¯é请 |
| | | * - å·¥å
·èåï¼Nginx é
ç½®å导 + ç¯å¢æ£æµ |
| | | */ |
| | | |
| | | defined('ABSPATH') || exit; |
| | | |
| | | /* ============================================================ |
| | | 注åèå |
| | | ============================================================ */ |
| | | add_action('admin_menu', function () { |
| | | add_menu_page('åé人管ç', 'åé人管ç', 'edit_others_posts', 'im-candidates', 'im_admin_list', 'dashicons-groups', 30); |
| | | add_submenu_page('im-candidates', 'åé人å表', 'åé人å表', 'edit_others_posts', 'im-candidates', 'im_admin_list'); |
| | | add_submenu_page('im-candidates', 'åé人详æ
', '', 'edit_others_posts', 'im-candidate-detail', 'im_admin_detail'); |
| | | add_submenu_page('tools.php', 'IM æå¡å¨é
ç½®', 'IM æå¡å¨é
ç½®', 'manage_options', 'im-server-setup', 'im_server_setup'); |
| | | }); |
| | | |
| | | /* ============================================================ |
| | | åå°æ ·å¼ + JSï¼åªå¨ç®¡ç页é¢å è½½ï¼ |
| | | ============================================================ */ |
| | | add_action('admin_head', function () { |
| | | $screen = get_current_screen(); |
| | | if (!$screen) |
| | | return; |
| | | $pages = ['toplevel_page_im-candidates', 'candidates_page_im-candidate-detail']; |
| | | if (!in_array($screen->id, $pages, true) && strpos($screen->id, 'im-candidates') === false && strpos($screen->id, 'im-candidate-detail') === false) |
| | | return; |
| | | ?> |
| | | <style> |
| | | /* åºç¡åè¡¨æ ·å¼ä¿ç */ |
| | | .im-a { |
| | | max-width: 1200px |
| | | } |
| | | |
| | | .im-a .widefat td { |
| | | vertical-align: middle; |
| | | padding: 11px 10px |
| | | } |
| | | |
| | | .im-badge { |
| | | display: inline-block; |
| | | padding: 3px 10px; |
| | | border-radius: 999px; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | border: 1px solid; |
| | | white-space: nowrap |
| | | } |
| | | |
| | | .im-tag { |
| | | display: inline-block; |
| | | background: #eff6ff; |
| | | color: #1e40af; |
| | | border: 1px solid #bfdbfe; |
| | | border-radius: 6px; |
| | | padding: 4px 10px; |
| | | font-size: 13px; |
| | | margin: 3px 4px 3px 0; |
| | | font-weight: 500 |
| | | } |
| | | |
| | | .im-muted { |
| | | color: #9ca3af; |
| | | font-style: italic |
| | | } |
| | | |
| | | .im-sub-cnt { |
| | | font-size: 12px; |
| | | color: #6b7280; |
| | | display: block; |
| | | margin-bottom: 3px |
| | | } |
| | | |
| | | .im-empty { |
| | | text-align: center; |
| | | color: #9ca3af; |
| | | padding: 36px !important; |
| | | font-size: 15px |
| | | } |
| | | |
| | | /* é«çº§è¯¦æ
é¡µæ ·å¼ */ |
| | | .im-detail-wrap { |
| | | margin-top: 20px; |
| | | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; |
| | | background: #f3f4f6; |
| | | padding: 24px; |
| | | border-radius: 12px; |
| | | box-sizing: border-box |
| | | } |
| | | |
| | | .im-detail-wrap * { |
| | | box-sizing: border-box |
| | | } |
| | | |
| | | .im-back-link { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | color: #4b5563; |
| | | text-decoration: none; |
| | | font-weight: 600; |
| | | font-size: 14px; |
| | | margin-bottom: 24px; |
| | | transition: color 0.2s |
| | | } |
| | | |
| | | .im-back-link:hover { |
| | | color: #009dff |
| | | } |
| | | |
| | | .im-hero-card { |
| | | background: #fff; |
| | | border-radius: 16px; |
| | | padding: 32px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04); |
| | | margin-bottom: 24px; |
| | | position: relative; |
| | | overflow: hidden |
| | | } |
| | | |
| | | .im-hero-card::before { |
| | | content: ""; |
| | | position: absolute; |
| | | top: 0; |
| | | bottom: 0; |
| | | left: 0; |
| | | width: 6px; |
| | | background: linear-gradient(180deg, #009dff, #53eee0) |
| | | } |
| | | |
| | | .im-hero-profile { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 24px |
| | | } |
| | | |
| | | .im-avatar { |
| | | width: 80px; |
| | | height: 80px; |
| | | border-radius: 50%; |
| | | background: linear-gradient(135deg, #e0f2fe, #bae6fd); |
| | | color: #0369a1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 28px; |
| | | font-weight: 700; |
| | | flex-shrink: 0 |
| | | } |
| | | |
| | | .im-hero-info h1 { |
| | | margin: 0 0 8px; |
| | | font-size: 26px; |
| | | font-weight: 700; |
| | | color: #111827; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 0 |
| | | } |
| | | |
| | | .im-hero-meta { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 16px; |
| | | color: #4b5563; |
| | | font-size: 14px |
| | | } |
| | | |
| | | .im-hero-meta-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px |
| | | } |
| | | |
| | | .im-hero-meta-item svg { |
| | | color: #9ca3af |
| | | } |
| | | |
| | | .im-main-grid { |
| | | display: grid; |
| | | grid-template-columns: 1fr 400px; |
| | | gap: 24px |
| | | } |
| | | |
| | | @media(max-width:1100px) { |
| | | .im-main-grid { |
| | | grid-template-columns: 1fr |
| | | } |
| | | } |
| | | |
| | | .im-card-modern { |
| | | background: #fff; |
| | | border-radius: 16px; |
| | | box-shadow: 0 3px 15px rgba(0, 0, 0, 0.03); |
| | | padding: 24px; |
| | | margin-bottom: 24px |
| | | } |
| | | |
| | | .im-card-title { |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | color: #111827; |
| | | margin: 0 0 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px |
| | | } |
| | | |
| | | .im-card-title svg { |
| | | color: #009dff |
| | | } |
| | | |
| | | .im-info-grid { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | gap: 20px |
| | | } |
| | | |
| | | .im-info-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 6px |
| | | } |
| | | |
| | | .im-info-label { |
| | | font-size: 13px; |
| | | color: #6b7280; |
| | | font-weight: 600; |
| | | text-transform: uppercase; |
| | | letter-spacing: 0.5px |
| | | } |
| | | |
| | | .im-info-val { |
| | | font-size: 15px; |
| | | color: #111827; |
| | | font-weight: 500; |
| | | line-height: 1.5 |
| | | } |
| | | |
| | | .im-info-val.full-width { |
| | | grid-column: 1/-1 |
| | | } |
| | | |
| | | .im-timeline { |
| | | position: relative; |
| | | padding-left: 24px; |
| | | border-left: 2px solid #e5e7eb; |
| | | margin-top: 12px |
| | | } |
| | | |
| | | .im-timeline-item { |
| | | position: relative; |
| | | margin-bottom: 24px |
| | | } |
| | | |
| | | .im-timeline-item:last-child { |
| | | margin-bottom: 0 |
| | | } |
| | | |
| | | .im-timeline-icon { |
| | | position: absolute; |
| | | left: -31px; |
| | | top: 0; |
| | | width: 14px; |
| | | height: 14px; |
| | | background: #fff; |
| | | border: 3px solid #009dff; |
| | | border-radius: 50% |
| | | } |
| | | |
| | | .im-timeline-date { |
| | | font-size: 13px; |
| | | color: #009dff; |
| | | font-weight: 700; |
| | | margin-bottom: 4px |
| | | } |
| | | |
| | | .im-timeline-title { |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | color: #111827; |
| | | margin: 0 0 4px |
| | | } |
| | | |
| | | .im-timeline-sub { |
| | | font-size: 14px; |
| | | color: #4b5563 |
| | | } |
| | | |
| | | .im-doc-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | padding: 12px 16px; |
| | | background: #f9fafb; |
| | | border: 1px solid #f3f4f6; |
| | | border-radius: 10px; |
| | | margin-bottom: 12px; |
| | | transition: all 0.2s |
| | | } |
| | | |
| | | .im-doc-item:hover { |
| | | border-color: #bfdbfe; |
| | | background: #eff6ff |
| | | } |
| | | |
| | | .im-doc-info { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | font-size: 14px; |
| | | color: #111827; |
| | | font-weight: 500; |
| | | overflow: hidden |
| | | } |
| | | |
| | | .im-doc-info svg { |
| | | color: #ef4444; |
| | | flex-shrink: 0 |
| | | } |
| | | |
| | | .im-doc-name { |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis |
| | | } |
| | | |
| | | .im-doc-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px |
| | | } |
| | | |
| | | .im-doc-btn { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 32px; |
| | | height: 32px; |
| | | border-radius: 6px; |
| | | background: #fff; |
| | | color: #4b5563; |
| | | border: 1px solid #d1d5db; |
| | | text-decoration: none; |
| | | transition: all 0.2s |
| | | } |
| | | |
| | | .im-doc-btn:hover { |
| | | background: #f3f4f6; |
| | | color: #111827 |
| | | } |
| | | |
| | | .im-token-box { |
| | | background: #f8fafc; |
| | | border: 1px solid #e2e8f0; |
| | | border-radius: 12px; |
| | | padding: 20px; |
| | | margin-bottom: 16px; |
| | | position: relative |
| | | } |
| | | |
| | | .im-token-status { |
| | | position: absolute; |
| | | top: 20px; |
| | | right: 20px |
| | | } |
| | | |
| | | .im-token-date { |
| | | font-size: 14px; |
| | | color: #374151; |
| | | font-weight: 600; |
| | | margin-bottom: 4px |
| | | } |
| | | |
| | | .im-token-exp { |
| | | font-size: 13px; |
| | | color: #64748b; |
| | | margin-bottom: 16px |
| | | } |
| | | |
| | | .im-video-player { |
| | | width: 100%; |
| | | border-radius: 8px; |
| | | background: #0f172a; |
| | | max-height: 240px; |
| | | margin-bottom: 12px |
| | | } |
| | | |
| | | .im-video-footer { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | font-size: 13px; |
| | | color: #4b5563 |
| | | } |
| | | |
| | | .im-video-dl { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | color: #009dff; |
| | | text-decoration: none; |
| | | font-weight: 600 |
| | | } |
| | | |
| | | .im-video-dl:hover { |
| | | text-decoration: underline |
| | | } |
| | | |
| | | .im-action-box { |
| | | background: linear-gradient(135deg, #f0f9ff, #e0f2fe); |
| | | border: 1px solid #bae6fd; |
| | | border-radius: 16px; |
| | | padding: 24px; |
| | | text-align: center; |
| | | margin-bottom: 24px |
| | | } |
| | | |
| | | .im-action-box h3 { |
| | | margin: 0 0 12px; |
| | | font-size: 18px; |
| | | color: #0369a1; |
| | | font-weight: 700 |
| | | } |
| | | |
| | | .im-action-box p { |
| | | color: #0284c7; |
| | | font-size: 14px; |
| | | margin: 0 0 20px |
| | | } |
| | | |
| | | .im-btn-primary { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | background: linear-gradient(135deg, #009dff, #3de1fe); |
| | | color: #fff; |
| | | border: none; |
| | | padding: 12px 24px; |
| | | border-radius: 8px; |
| | | font-size: 15px; |
| | | font-weight: 700; |
| | | cursor: pointer; |
| | | transition: all 0.3s; |
| | | box-shadow: 0 4px 12px rgba(0, 157, 255, 0.25); |
| | | width: 100%; |
| | | margin-bottom: 12px |
| | | } |
| | | |
| | | .im-btn-primary:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 6px 16px rgba(0, 157, 255, 0.35); |
| | | color: #fff |
| | | } |
| | | |
| | | .im-btn-danger { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | background: linear-gradient(135deg, #ef4444, #f87171); |
| | | color: #fff; |
| | | border: none; |
| | | padding: 12px 24px; |
| | | border-radius: 8px; |
| | | font-size: 15px; |
| | | font-weight: 700; |
| | | cursor: pointer; |
| | | transition: all 0.3s; |
| | | box-shadow: 0 4px 12px rgba(239, 68, 68, 0.25); |
| | | width: 100% |
| | | } |
| | | |
| | | .im-btn-danger:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 6px 16px rgba(239, 68, 68, 0.35); |
| | | color: #fff |
| | | } |
| | | |
| | | .im-btn-success { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | background: linear-gradient(135deg, #10b981, #34d399); |
| | | color: #fff; |
| | | border: none; |
| | | padding: 12px 24px; |
| | | border-radius: 8px; |
| | | font-size: 15px; |
| | | font-weight: 700; |
| | | cursor: pointer; |
| | | transition: all 0.3s; |
| | | box-shadow: 0 4px 12px rgba(16, 185, 129, 0.25); |
| | | width: 100% |
| | | } |
| | | |
| | | .im-btn-success:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 6px 16px rgba(16, 185, 129, 0.35); |
| | | color: #fff |
| | | } |
| | | |
| | | .im-modal-bg { |
| | | position: fixed; |
| | | inset: 0; |
| | | background: rgba(17, 24, 39, 0.7); |
| | | backdrop-filter: blur(4px); |
| | | z-index: 99999; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center |
| | | } |
| | | |
| | | .im-modal { |
| | | background: #fff; |
| | | border-radius: 16px; |
| | | padding: 32px; |
| | | max-width: 440px; |
| | | width: 90%; |
| | | box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25) |
| | | } |
| | | |
| | | .im-modal h3 { |
| | | margin: 0 0 12px; |
| | | font-size: 20px; |
| | | font-weight: 700; |
| | | color: #111827 |
| | | } |
| | | |
| | | .im-modal p { |
| | | color: #4b5563; |
| | | margin: 0 0 24px; |
| | | line-height: 1.6 |
| | | } |
| | | |
| | | .im-modal textarea { |
| | | width: 100%; |
| | | padding: 12px; |
| | | border: 1px solid #d1d5db; |
| | | border-radius: 8px; |
| | | margin-bottom: 20px; |
| | | min-height: 80px; |
| | | font-family: inherit |
| | | } |
| | | |
| | | .im-modal-btns { |
| | | display: flex; |
| | | gap: 12px; |
| | | justify-content: flex-end |
| | | } |
| | | |
| | | .im-btn-cancel { |
| | | background: #f3f4f6; |
| | | color: #374151; |
| | | border: none; |
| | | padding: 10px 20px; |
| | | border-radius: 8px; |
| | | font-weight: 600; |
| | | cursor: pointer |
| | | } |
| | | |
| | | .im-btn-cancel:hover { |
| | | background: #e5e7eb |
| | | } |
| | | |
| | | .im-btn-confirm { |
| | | background: #009dff; |
| | | color: #fff; |
| | | border: none; |
| | | padding: 10px 20px; |
| | | border-radius: 8px; |
| | | font-weight: 600; |
| | | cursor: pointer |
| | | } |
| | | |
| | | .im-btn-confirm:hover { |
| | | background: #0284c7 |
| | | } |
| | | |
| | | .im-reject-reason { |
| | | background: #fef2f2; |
| | | border: 1px solid #fecaca; |
| | | color: #991b1b; |
| | | padding: 16px; |
| | | border-radius: 8px; |
| | | margin-bottom: 24px; |
| | | font-size: 14px |
| | | } |
| | | </style> |
| | | <script> |
| | | jQuery(function ($) { |
| | | var cid = null, actionType = ''; |
| | | $(document).on('click', '.im-action-btn', function () { |
| | | cid = $(this).data('id'); |
| | | actionType = $(this).data('action'); |
| | | var name = $(this).data('name'); |
| | | var title = '', body = '', showTextarea = false; |
| | | |
| | | if (actionType === 'invite') { |
| | | title = '确认åéé¢è¯é请'; |
| | | body = 'å°å <strong>' + name + '</strong> åéé¢è¯é请ï¼<br>é®ä»¶å
å« <strong>24å°æ¶ææ</strong> çä¸å±é¢è¯é¾æ¥ï¼ç¡®è®¤åéï¼'; |
| | | } else if (actionType === 'reject') { |
| | | title = 'æç»åé人'; |
| | | body = '确认æç» <strong>' + name + '</strong> çç³è¯·ï¼<br>ç³»ç»å°åå
¶åéæç»éç¥é®ä»¶ã'; |
| | | } else if (actionType === 'hire') { |
| | | title = 'å½ååé人'; |
| | | body = '确认æ£å¼å½å <strong>' + name + '</strong>ï¼<br>ç³»ç»å°åå
¶åéå½åéç¥é®ä»¶åå¹è®é¾æ¥ã'; |
| | | } else if (actionType === 'resend_joinus') { |
| | | title = 'éæ°åéé®ä»¶'; |
| | | body = 'å°éæ°å <strong>' + name + '</strong> åéâå®å详ç»è¡¨åâ龿¥é®ä»¶ã'; |
| | | } else if (actionType === 'resend_training') { |
| | | title = 'éæ°åéå¹è®é¾æ¥'; |
| | | body = 'å°éæ°å <strong>' + name + '</strong> åéå¹è®é¾æ¥é®ä»¶ã'; |
| | | } else if (actionType === 'resend_trained_email') { |
| | | title = 'éæ°åéè´¦å·é®ä»¶'; |
| | | body = 'å°éæ°å <strong>' + name + '</strong> åé Microsoft Teams 临æ¶è´¦å·é®ä»¶ã'; |
| | | } else if (actionType === 'delete') { |
| | | title = 'å é¤åé人'; |
| | | body = '确认永ä¹
å é¤ <strong>' + name + '</strong> çææè®°å½ï¼<br><span style=âcolor:#ef4444â>æ¤æä½ä¸å¯æ¢å¤ï¼å°åæ¶å é¤è¯¥åé人ä¸ä¼ çæææä»¶ï¼éä»¶ãé¢è¯è§é¢çï¼ã</span>'; |
| | | } |
| | | |
| | | $('#im-modal h3').text(title); |
| | | $('#im-modal-body').html(body); |
| | | if (showTextarea) { |
| | | $('#im-modal-textarea').show().val(''); |
| | | } else { |
| | | $('#im-modal-textarea').hide(); |
| | | } |
| | | $('#im-modal').fadeIn(150); |
| | | }); |
| | | |
| | | $(document).on('click', '.im-modal-cancel,.im-modal-bg', function (e) { if (e.target === this || $(this).hasClass('im-modal-cancel')) $('#im-modal').fadeOut(150); }); |
| | | $(document).on('keydown', function (e) { if (e.key === 'Escape') $('#im-modal').fadeOut(150); }); |
| | | |
| | | $('#im-modal-ok').on('click', function (e) { |
| | | e.preventDefault(); |
| | | if (!cid) return; |
| | | var reason = $('#im-modal-textarea').is(':visible') ? $.trim($('#im-modal-textarea').val()) : ''; |
| | | |
| | | var $b = $(this); $b.prop('disabled', true).text('å¤çä¸...'); |
| | | |
| | | var $modal = $('#im-modal'); |
| | | var ajaxActUrl = $modal.data('ajaxurl') || '/wp-admin/admin-ajax.php'; |
| | | var ajaxNonce = $modal.data('nonce') || ''; |
| | | |
| | | var ajaxAction = 'im_action_' + actionType; |
| | | $.ajax({ |
| | | url: ajaxActUrl, |
| | | type: 'POST', |
| | | dataType: 'json', |
| | | data: { action: ajaxAction, nonce: ajaxNonce, candidate_id: cid, reason: reason }, |
| | | success: function (r) { |
| | | $('#im-modal').fadeOut(150); $b.prop('disabled', false).text('确认'); |
| | | var isSuccess = r && r.success; |
| | | var msg = r && r.data && r.data.message ? ((isSuccess ? 'â
' : 'â ') + r.data.message) : (isSuccess ? 'â
æä½æå' : 'â æä½å¤±è´¥ï¼è¯·éè¯'); |
| | | var $n = $('<div class="notice ' + (isSuccess ? 'notice-success' : 'notice-error') + ' is-dismissible" style="margin-top:16px"><p>' + msg + '</p></div>'); |
| | | if ($('.wp-heading-inline').length) { |
| | | $('.wp-heading-inline').after($n); |
| | | } else if ($('.im-detail-wrap').length) { |
| | | $('.im-detail-wrap').prepend($n); |
| | | } |
| | | var $res = $('#im-res-' + cid); |
| | | if ($res.length) $res.removeClass('ok err').addClass(isSuccess ? 'ok' : 'err').text(msg); |
| | | if (isSuccess) setTimeout(function () { location.reload(); }, 1000); |
| | | else setTimeout(function () { $n.fadeOut(400, function () { $(this).remove(); }); }, 5000); |
| | | }, |
| | | error: function (xhr, status, error) { |
| | | $('#im-modal').fadeOut(150); $b.prop('disabled', false).text('确认'); |
| | | var errMsg = 'ç½ç»ç¹å¿ææå¡å¨æ¥éï¼è¯·éè¯ã'; |
| | | if (xhr.status === 403) errMsg = '请æ±è¢«æç»(403 Forbidden)ãè¿é常æ¯å 为å®å
¨æä»¶(å¦Wordfence/å®å¡WAF)æ¦æªäºæºå¸¦åæ°ç请æ±ï¼è¯·å°è¯å°åå°å å
¥ç½ååæè系管çåéæ¾æ¦æªã'; |
| | | alert(errMsg); |
| | | } |
| | | }); |
| | | }); |
| | | }); |
| | | </script> |
| | | <?php |
| | | }); |
| | | |
| | | /* ============================================================ |
| | | AJAX å¤ç |
| | | ============================================================ */ |
| | | add_action('wp_ajax_im_action_invite', function () { |
| | | check_ajax_referer('im_admin_nonce', 'nonce'); |
| | | if (!current_user_can('edit_others_posts')) |
| | | wp_send_json_error(['message' => 'æéä¸è¶³']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | |
| | | $c = IM_Candidate::get($id); |
| | | if ($c && $c->status === 'screening') { |
| | | IM_Candidate::update_status($id, 'invited'); |
| | | } |
| | | IM_Mailer::send_interview_invite($id) |
| | | ? wp_send_json_success(['message' => 'é¢è¯é请é®ä»¶å·²æååéï¼']) |
| | | : wp_send_json_error(['message' => 'é®ä»¶åé失败ï¼è¯·æ£æ¥é®ä»¶é
ç½®ã']); |
| | | }); |
| | | |
| | | add_action('wp_ajax_im_action_reject', function () { |
| | | check_ajax_referer('im_admin_nonce', 'nonce'); |
| | | if (!current_user_can('edit_others_posts')) |
| | | wp_send_json_error(['message' => 'æéä¸è¶³']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | IM_Candidate::update_status($id, 'rejected'); |
| | | IM_Mailer::send_reject($id) |
| | | ? wp_send_json_success(['message' => 'å·²æç»è¯¥åé人并åéé®ä»¶ï¼']) |
| | | : wp_send_json_error(['message' => 'å·²æ´æ°ç¶æï¼ä½é®ä»¶åé失败ã']); |
| | | }); |
| | | |
| | | add_action('wp_ajax_im_action_hire', function () { |
| | | check_ajax_referer('im_admin_nonce', 'nonce'); |
| | | if (!current_user_can('edit_others_posts')) |
| | | wp_send_json_error(['message' => 'æéä¸è¶³']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | IM_Mailer::send_hire($id) |
| | | ? wp_send_json_success(['message' => 'å·²å½å该åé人并åéå¹è®é¾æ¥é®ä»¶ï¼']) |
| | | : wp_send_json_error(['message' => 'å½åæä½å¤±è´¥ï¼è¯·æ£æ¥é®ä»¶é
ç½®ã']); |
| | | }); |
| | | |
| | | add_action('wp_ajax_im_action_resend_joinus', function () { |
| | | check_ajax_referer('im_admin_nonce', 'nonce'); |
| | | if (!current_user_can('edit_others_posts')) |
| | | wp_send_json_error(['message' => 'æéä¸è¶³']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | IM_Mailer::send_joinus_confirmation($id) |
| | | ? wp_send_json_success(['message' => '表å龿¥é®ä»¶å·²éæ°åéï¼']) |
| | | : wp_send_json_error(['message' => 'é®ä»¶åé失败ã']); |
| | | }); |
| | | |
| | | add_action('wp_ajax_im_action_resend_training', function () { |
| | | check_ajax_referer('im_admin_nonce', 'nonce'); |
| | | if (!current_user_can('edit_others_posts')) |
| | | wp_send_json_error(['message' => 'æéä¸è¶³']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | $c = IM_Candidate::get($id); |
| | | if (!$c || $c->status !== 'training') |
| | | wp_send_json_error(['message' => '该åé人ä¸å¨å¹è®ç¶æ']); |
| | | IM_Mailer::send_hire($id) |
| | | ? wp_send_json_success(['message' => 'å¹è®é¾æ¥é®ä»¶å·²éæ°åéï¼']) |
| | | : wp_send_json_error(['message' => 'é®ä»¶åé失败ã']); |
| | | }); |
| | | |
| | | add_action('wp_ajax_im_action_resend_trained_email', function () { |
| | | check_ajax_referer('im_admin_nonce', 'nonce'); |
| | | if (!current_user_can('edit_others_posts')) |
| | | wp_send_json_error(['message' => 'æéä¸è¶³']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | $c = IM_Candidate::get($id); |
| | | if (!$c || $c->status !== 'trained') |
| | | wp_send_json_error(['message' => '该åé人ä¸å¨å·²å®æå¹è®ç¶æ']); |
| | | IM_Mailer::send_training_complete($id) |
| | | ? wp_send_json_success(['message' => 'è´¦å·é®ä»¶å·²éæ°åéï¼']) |
| | | : wp_send_json_error(['message' => 'é®ä»¶åé失败ã']); |
| | | }); |
| | | |
| | | add_action('wp_ajax_im_action_delete', function () { |
| | | check_ajax_referer('im_admin_nonce', 'nonce'); |
| | | if (!current_user_can('edit_others_posts')) |
| | | wp_send_json_error(['message' => 'æéä¸è¶³']); |
| | | $id = (int) ($_POST['candidate_id'] ?? 0); |
| | | if (!$id) |
| | | wp_send_json_error(['message' => 'åæ°é误']); |
| | | IM_Candidate::delete_with_files($id) |
| | | ? wp_send_json_success(['message' => 'åé人记å½åå
³èæä»¶å·²å é¤ï¼']) |
| | | : wp_send_json_error(['message' => 'å é¤å¤±è´¥ï¼è¯·éè¯ã']); |
| | | }); |
| | | |
| | | /* ============================================================ |
| | | åé人å表页 |
| | | ============================================================ */ |
| | | function im_admin_list() |
| | | { |
| | | $page = max(1, (int) ($_GET['paged'] ?? 1)); |
| | | $status = sanitize_text_field($_GET['status'] ?? ''); |
| | | $search = sanitize_text_field($_GET['s'] ?? ''); |
| | | $per_page = 20; |
| | | $args = compact('page', 'status', 'search') + ['per_page' => $per_page]; |
| | | $items = IM_Candidate::get_list($args); |
| | | $total = IM_Candidate::count($args); |
| | | $pages = (int) ceil($total / $per_page); |
| | | |
| | | $cnt = [ |
| | | '' => IM_Candidate::count(), |
| | | 'applied' => IM_Candidate::count(['status' => 'applied']), |
| | | 'screening' => IM_Candidate::count(['status' => 'screening']), |
| | | 'invited' => IM_Candidate::count(['status' => 'invited']), |
| | | 'rejected' => IM_Candidate::count(['status' => 'rejected']), |
| | | 'completed' => IM_Candidate::count(['status' => 'completed']), |
| | | 'hired' => IM_Candidate::count(['status' => 'hired']), |
| | | 'training' => IM_Candidate::count(['status' => 'training']), |
| | | 'trained' => IM_Candidate::count(['status' => 'trained']), |
| | | ]; |
| | | $tabs = [ |
| | | '' => 'å
¨é¨', |
| | | 'applied' => 'å·²ç³è¯·', |
| | | 'screening' => 'å¾
çé', |
| | | 'invited' => 'å·²é请', |
| | | 'rejected' => 'å·²æç»', |
| | | 'completed' => '已宿', |
| | | 'hired' => 'å·²å½å', |
| | | 'training' => 'æªå®æå¹è®', |
| | | 'trained' => '已宿å¹è®' |
| | | ]; |
| | | |
| | | $status_lbl = [ |
| | | 'applied' => 'å¾
å®åä¿¡æ¯', |
| | | 'screening' => 'å·²æäº¤è¯¦ç»ä¿¡æ¯', |
| | | 'invited' => 'å·²é请', |
| | | 'rejected' => 'å·²æç»', |
| | | 'completed' => '已宿', |
| | | 'hired' => 'å·²å½å', |
| | | 'training' => 'æªå®æå¹è®', |
| | | 'trained' => '已宿å¹è®' |
| | | ]; |
| | | |
| | | $colors = [ |
| | | 'applied' => '#f59e0b', |
| | | 'screening' => '#8b5cf6', |
| | | 'invited' => '#3b82f6', |
| | | 'rejected' => '#ef4444', |
| | | 'completed' => '#10b981', |
| | | 'hired' => '#059669', |
| | | 'training' => '#f97316', |
| | | 'trained' => '#06b6d4' |
| | | ]; |
| | | ?> |
| | | <div class="wrap im-a"> |
| | | <h1 class="wp-heading-inline">åé人管ç</h1> |
| | | <hr class="wp-header-end"> |
| | | |
| | | <ul class="subsubsub"> |
| | | <?php foreach ($tabs as $key => $lbl): |
| | | $url = admin_url('admin.php?page=im-candidates' . ($key ? '&status=' . $key : '')); |
| | | ?> |
| | | <li><a href="<?= esc_url($url) ?>" <?= $status === $key ? 'class="current"' : '' ?>> |
| | | <?= $lbl ?> <span class="count">(<?= $cnt[$key] ?>)</span> |
| | | </a><?= $key !== 'trained' ? ' |' : '' ?></li> |
| | | <?php endforeach; ?> |
| | | </ul> |
| | | |
| | | <form method="get" style="margin:8px 0 16px"> |
| | | <input type="hidden" name="page" value="im-candidates"> |
| | | <?php if ($status): ?><input type="hidden" name="status" value="<?= esc_attr($status) ?>"><?php endif; ?> |
| | | <p class="search-box" style="margin:0"> |
| | | <input type="search" name="s" value="<?= esc_attr($search) ?>" placeholder="æç´¢å§åæé®ç®±..." style="width:240px"> |
| | | <button type="submit" class="button">æç´¢</button> |
| | | </p> |
| | | </form> |
| | | |
| | | <table class="wp-list-table widefat fixed striped"> |
| | | <thead> |
| | | <tr> |
| | | <th width="160">å§å</th> |
| | | <th>é®ç®±</th> |
| | | <th width="140">æ´æ°æ¶é´</th> |
| | | <th width="120">ç¶æ</th> |
| | | <th>æè¯¾ç§ç® / 详æ
</th> |
| | | <th width="240">æä½</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <?php if ($items): |
| | | foreach ($items as $c): |
| | | $detail = admin_url('admin.php?page=im-candidate-detail&id=' . $c->id); |
| | | $subs = IM_Candidate::get_subjects($c); |
| | | $color = $colors[$c->status] ?? '#6b7280'; |
| | | $clabel = $status_lbl[$c->status] ?? $c->status; |
| | | $name_disp = $c->status === 'applied' ? esc_html($c->last_name . ' ' . $c->first_name) : '<a href="' . esc_url($detail) . '">' . esc_html($c->last_name . ' ' . $c->first_name) . '</a>'; |
| | | ?> |
| | | <tr> |
| | | <td> |
| | | <strong><?= $name_disp ?></strong> |
| | | <?php if ($c->preferred_name): ?> |
| | | <span style="color:#9ca3af;font-size:12px">(<?= esc_html($c->preferred_name) ?>)</span> |
| | | <?php endif; ?> |
| | | </td> |
| | | <td><a href="mailto:<?= esc_attr($c->email) ?>"><?= esc_html($c->email) ?></a></td> |
| | | <td style="font-size:13px"><?= esc_html(date('Y-m-d H:i', strtotime($c->updated_at))) ?></td> |
| | | <td> |
| | | <span class="im-badge" |
| | | style="background:<?= $color ?>18;color:<?= $color ?>;border-color:<?= $color ?>44"><?= $clabel ?></span> |
| | | <?php if ($c->status === 'applied' && !empty($c->apply_opened_at)): ?> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â å·²æå¼è¡¨å</div> |
| | | <?php elseif ($c->status === 'applied' && empty($c->apply_opened_at)): ?> |
| | | <div style="font-size:12px;color:#9ca3af;margin-top:4px">â æªæå¼è¡¨å</div> |
| | | <?php elseif ($c->status === 'invited'): ?> |
| | | <?php $tk_list = IM_Token::get_by_candidate($c->id); |
| | | if (!empty($tk_list)): |
| | | $tk = $tk_list[0]; ?> |
| | | <?php if (!empty($tk->opened_at)): ?> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â å·²æ¥çé¢è¯é¢</div> |
| | | <?php else: ?> |
| | | <div style="font-size:12px;color:#9ca3af;margin-top:4px">â æªç¹å¼é¢è¯é¾æ¥</div> |
| | | <?php endif; ?> |
| | | <?php endif; ?> |
| | | <?php elseif ($c->status === 'training'): ?> |
| | | <?php if (!empty($c->training_opened_at)): ?> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â å·²æå¼å¹è®é¡µé¢</div> |
| | | <?php else: ?> |
| | | <div style="font-size:12px;color:#9ca3af;margin-top:4px">â æªæå¼å¹è®é¡µé¢</div> |
| | | <?php endif; ?> |
| | | <?php elseif ($c->status === 'trained'): ?> |
| | | <?php if (!empty($c->training_completed_at)): ?> |
| | | <div style="font-size:12px;color:#059669;margin-top:4px">â <?= date('m/d H:i', strtotime($c->training_completed_at)) ?> 宿</div> |
| | | <?php endif; ?> |
| | | <?php endif; ?> |
| | | </td> |
| | | <td> |
| | | <?php if ($c->status === 'applied'): ?> |
| | | <span class="im-muted">çå¾
å®åä¿¡æ¯</span> |
| | | <?php elseif ($subs): ?> |
| | | <?php foreach (array_slice($subs, 0, 3) as $s): ?> |
| | | <span class="im-tag"><?= esc_html($s) ?></span> |
| | | <?php endforeach; ?> |
| | | <?php if (count($subs) > 3): ?><span |
| | | class="im-tag im-tag-more">+<?= count($subs) - 3 ?></span><?php endif; ?> |
| | | <?php else: ?><span class="im-muted">â</span><?php endif; ?> |
| | | </td> |
| | | <td> |
| | | <?php if ($c->status !== 'applied'): ?> |
| | | <a href="<?= esc_url($detail) ?>" class="button button-small">æ¥ç详æ
</a> |
| | | <?php endif; ?> |
| | | |
| | | <?php if ($c->status === 'applied'): ?> |
| | | <button class="button button-small im-action-btn" data-action="resend_joinus" |
| | | data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->last_name . ' ' . $c->first_name) ?>">éæ°åéé®ä»¶</button> |
| | | <?php elseif ($c->status === 'screening'): ?> |
| | | <button class="button button-small button-primary im-action-btn" data-action="invite" |
| | | data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">åéé¢è¯é请</button> |
| | | <button class="button button-small im-action-btn" data-action="reject" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">æç»</button> |
| | | <?php elseif ($c->status === 'invited'): ?> |
| | | <button class="button button-small im-action-btn" data-action="invite" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">éæ°åéé请</button> |
| | | <button class="button button-small im-action-btn" data-action="reject" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">æç»</button> |
| | | <?php elseif ($c->status === 'completed'): ?> |
| | | <button class="button button-small im-action-btn" style="border-color:#10b981;color:#10b981" |
| | | data-action="hire" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">å½å</button> |
| | | <button class="button button-small im-action-btn" data-action="reject" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">æç»</button> |
| | | <?php elseif ($c->status === 'training'): ?> |
| | | <button class="button button-small im-action-btn" data-action="resend_training" |
| | | data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">éæ°åéå¹è®é¾æ¥</button> |
| | | <?php elseif ($c->status === 'trained'): ?> |
| | | <button class="button button-small im-action-btn" data-action="resend_trained_email" |
| | | data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->preferred_name ?: $c->first_name) ?>">éæ°åéè´¦å·é®ä»¶</button> |
| | | <?php endif; ?> |
| | | |
| | | <button class="button button-small im-action-btn" style="color:#ef4444;border-color:#fca5a5" |
| | | data-action="delete" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($c->last_name . ' ' . $c->first_name) ?>">å é¤</button> |
| | | |
| | | <?php if (defined('IM_DEBUG_LINKS') && IM_DEBUG_LINKS): ?> |
| | | <div |
| | | style="margin-top:12px;padding-top:8px;border-top:1px dashed #e5e7eb;font-size:12px;word-break:break-all;line-height:1.5"> |
| | | <?php if (empty($c->apply_token_used) && $c->apply_token): ?> |
| | | <div style="color:#6b7280;margin-bottom:4px">è°è¯: 表å龿¥ <br><a |
| | | href="<?= esc_url(add_query_arg('im_apply_token', $c->apply_token, IM_APPLY_PAGE_URL)) ?>" |
| | | target="_blank" |
| | | style="color:#8b5cf6"><?= esc_html(add_query_arg('im_apply_token', $c->apply_token, IM_APPLY_PAGE_URL)) ?></a> |
| | | </div> |
| | | <?php endif; ?> |
| | | <?php if (in_array($c->status, ['invited', 'completed', 'hired', 'rejected', 'training', 'trained'])): |
| | | $latest_token = IM_Token::get_by_candidate($c->id); |
| | | if (!empty($latest_token)): |
| | | $tk2 = $latest_token[0]; ?> |
| | | <div style="color:#6b7280">è°è¯: é¢è¯é¾æ¥ <br><a |
| | | href="<?= esc_url(add_query_arg('im_token', $tk2->token, IM_INTERVIEW_PAGE_URL)) ?>" target="_blank" |
| | | style="color:#3b82f6"><?= esc_html(add_query_arg('im_token', $tk2->token, IM_INTERVIEW_PAGE_URL)) ?></a> |
| | | </div> |
| | | <?php endif; endif; ?> |
| | | <?php if (in_array($c->status, ['training', 'trained']) && !empty($c->training_token)): ?> |
| | | <div style="color:#6b7280">è°è¯: å¹è®é¾æ¥ <br><a |
| | | href="<?= esc_url(add_query_arg('im_training_token', $c->training_token, IM_TRAINING_PAGE_URL)) ?>" target="_blank" |
| | | style="color:#f97316"><?= esc_html(add_query_arg('im_training_token', $c->training_token, IM_TRAINING_PAGE_URL)) ?></a> |
| | | </div> |
| | | <?php endif; ?> |
| | | </div> |
| | | <?php endif; ?> |
| | | </td> |
| | | </tr> |
| | | <?php endforeach; else: ?> |
| | | <tr> |
| | | <td colspan="6" class="im-empty">ææ åéäººæ°æ®</td> |
| | | </tr> |
| | | <?php endif; ?> |
| | | </tbody> |
| | | </table> |
| | | |
| | | <?php if ($pages > 1): |
| | | $base = admin_url('admin.php?page=im-candidates' . ($status ? '&status=' . $status : '') . ($search ? '&s=' . urlencode($search) : '')); |
| | | ?> |
| | | <div class="tablenav bottom"> |
| | | <div style="display:flex;gap:4px;padding:8px 0"> |
| | | <?php for ($i = 1; $i <= $pages; $i++): ?> |
| | | <a href="<?= $base ?>&paged=<?= $i ?>" |
| | | class="button <?= $i === $page ? 'button-primary' : '' ?>"><?= $i ?></a> |
| | | <?php endfor; ?> |
| | | </div> |
| | | </div> |
| | | <?php endif; ?> |
| | | </div> |
| | | <?php im_admin_modal(); ?> |
| | | <?php |
| | | } |
| | | |
| | | /* ============================================================ |
| | | åé人详æ
页 |
| | | ============================================================ */ |
| | | function im_admin_detail() |
| | | { |
| | | $id = (int) ($_GET['id'] ?? 0); |
| | | $c = $id ? IM_Candidate::get($id) : null; |
| | | if (!$c) { |
| | | echo '<div class="wrap"><div class="notice notice-error"><p>åé人ä¸åå¨ã</p></div></div>'; |
| | | return; |
| | | } |
| | | if ($c->status === 'applied') { |
| | | echo '<div class="wrap"><div class="notice notice-warning"><p>该åéäººå°æªæäº¤è¯¦ç»ç³è¯·è¡¨ï¼æ æ³æ¥ç详æ
ã</p></div><a href="' . admin_url('admin.php?page=im-candidates') . '" class="button">è¿åå表</a></div>'; |
| | | return; |
| | | } |
| | | |
| | | $subjects = IM_Candidate::get_subjects($c); |
| | | $tokens = IM_Token::get_by_candidate($id); |
| | | $attachments = IM_Attachment::get_by_candidate($id); |
| | | $upload_dir = wp_upload_dir(); |
| | | $status_lbl = [ |
| | | 'applied' => 'å¾
å®åä¿¡æ¯', |
| | | 'screening' => 'å·²æäº¤è¯¦ç»ä¿¡æ¯', |
| | | 'invited' => 'å·²é请', |
| | | 'rejected' => 'å·²æç»', |
| | | 'completed' => '已宿', |
| | | 'hired' => 'å·²å½å', |
| | | 'training' => 'æªå®æå¹è®', |
| | | 'trained' => '已宿å¹è®' |
| | | ]; |
| | | $status_colors = [ |
| | | 'applied' => ['bg' => '#fffbeb', 'text' => '#b45309', 'border' => '#fde68a'], |
| | | 'screening' => ['bg' => '#f3e8ff', 'text' => '#6d28d9', 'border' => '#e9d5ff'], |
| | | 'invited' => ['bg' => '#eff6ff', 'text' => '#1d4ed8', 'border' => '#bfdbfe'], |
| | | 'rejected' => ['bg' => '#fef2f2', 'text' => '#b91c1c', 'border' => '#fecaca'], |
| | | 'completed' => ['bg' => '#ecfdf5', 'text' => '#047857', 'border' => '#a7f3d0'], |
| | | 'hired' => ['bg' => '#ecfccb', 'text' => '#166534', 'border' => '#d9f99d'], |
| | | 'training' => ['bg' => '#fff7ed', 'text' => '#c2410c', 'border' => '#fed7aa'], |
| | | 'trained' => ['bg' => '#ecfeff', 'text' => '#0e7490', 'border' => '#a5f3fc'] |
| | | ]; |
| | | |
| | | $full_name = esc_html($c->first_name . ' ' . $c->last_name); |
| | | $p_name = esc_html($c->preferred_name); |
| | | $initials = mb_substr(trim($c->first_name), 0, 1) . mb_substr(trim($c->last_name), 0, 1); |
| | | if (!$initials && $full_name) |
| | | $initials = mb_substr($full_name, 0, 1); |
| | | $s_col = $status_colors[$c->status]; |
| | | ?> |
| | | <div class="im-a im-detail-wrap"> |
| | | <a href="<?= admin_url('admin.php?page=im-candidates') ?>" class="im-back-link"> |
| | | <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" |
| | | stroke-linecap="round" stroke-linejoin="round"> |
| | | <path d="M19 12H5m7 7l-7-7 7-7" /> |
| | | </svg> |
| | | è¿ååé人å表 |
| | | </a> |
| | | |
| | | <!-- Hero Card --> |
| | | <div class="im-hero-card"> |
| | | <div class="im-hero-profile"> |
| | | <div class="im-avatar"><?= strtoupper($initials) ?></div> |
| | | <div class="im-hero-info"> |
| | | <h1><?= $full_name ?> |
| | | <?= $p_name ? '<span style="color:#6b7280;font-size:18px;font-weight:500">(' . $p_name . ')</span>' : '' ?> |
| | | </h1> |
| | | <div class="im-hero-meta"> |
| | | <span class="im-hero-meta-item"> |
| | | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" /> |
| | | <path d="M22 6l-10 7L2 6" /> |
| | | </svg> |
| | | <a href="mailto:<?= esc_attr($c->email) ?>" |
| | | style="color:inherit;text-decoration:none"><?= esc_html($c->email) ?></a> |
| | | </span> |
| | | <?php if ($c->phone): ?> |
| | | <span class="im-hero-meta-item"> |
| | | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path |
| | | d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 16.92z" /> |
| | | </svg> |
| | | <?= esc_html($c->phone) ?> |
| | | </span> |
| | | <?php endif; ?> |
| | | <?php if (!empty($c->country) || !empty($c->city)): ?> |
| | | <span class="im-hero-meta-item"> |
| | | <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" /> |
| | | <circle cx="12" cy="10" r="3" /> |
| | | </svg> |
| | | <?= esc_html(trim(($c->city ?? '') . (!empty($c->city) && !empty($c->country) ? ', ' : '') . ($c->country ?? ''))) ?> |
| | | </span> |
| | | <?php endif; ?> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div> |
| | | <span class="im-badge" |
| | | style="background:<?= $s_col['bg'] ?>;color:<?= $s_col['text'] ?>;border-color:<?= $s_col['border'] ?>;padding:6px 16px;font-size:14px"> |
| | | <?= $status_lbl[$c->status] ?> |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="im-main-grid"> |
| | | <div class="im-col-main"> |
| | | <!-- 详æ
å¡çï¼æè²èæ¯ --> |
| | | <div class="im-card-modern"> |
| | | <h2 class="im-card-title"> |
| | | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| | | <path d="M22 10v6M2 10l10-5 10 5-10 5z" /> |
| | | <path d="M6 12v5c3 3 9 3 12 0v-5" /> |
| | | </svg> |
| | | æè²èæ¯ |
| | | </h2> |
| | | <div class="im-timeline"> |
| | | <div class="im-timeline-item"> |
| | | <div class="im-timeline-icon"></div> |
| | | <?php $degree_map = ["Bachelor's" => 'æ¬ç§', "Master's" => 'ç¡å£«', 'PhD' => 'å士']; ?> |
| | | <div class="im-timeline-date"><?= esc_html($c->grad_year ?: 'æªç¥å¹´ä»½') ?> æ¯ä¸ |
| | | (<?= esc_html($degree_map[$c->degree_level] ?? ($c->degree_level ?: 'æé«å¦å')) ?>)</div> |
| | | <h3 class="im-timeline-title"><?= esc_html($c->university ?: 'â') ?></h3> |
| | | <div class="im-timeline-sub">ä¸ä¸ï¼<?= esc_html($c->major ?: 'â') ?> | |
| | | GPAï¼<?= esc_html($c->gpa ?: 'â') ?> |
| | | <?= $c->deans_list ? ' | ð
Dean\'s List' : '' ?> |
| | | </div> |
| | | </div> |
| | | <?php if ($c->ug_university): ?> |
| | | <div class="im-timeline-item"> |
| | | <div class="im-timeline-icon" style="border-color:#d1d5db"></div> |
| | | <div class="im-timeline-date" style="color:#6b7280"><?= esc_html($c->ug_grad_year ?: 'æªç¥å¹´ä»½') ?> |
| | | æ¯ä¸ (æ¬ç§å¦å)</div> |
| | | <h3 class="im-timeline-title"><?= esc_html($c->ug_university) ?></h3> |
| | | <div class="im-timeline-sub">ä¸ä¸ï¼<?= esc_html($c->ug_major ?: 'â') ?></div> |
| | | </div> |
| | | <?php endif; ?> |
| | | <?php if (!empty($c->ms_university)): ?> |
| | | <div class="im-timeline-item"> |
| | | <div class="im-timeline-icon" style="border-color:#8b5cf6"></div> |
| | | <div class="im-timeline-date" style="color:#8b5cf6"><?= esc_html($c->ms_grad_year ?: 'æªç¥å¹´ä»½') ?> |
| | | æ¯ä¸ (ç¡å£«å¦å)</div> |
| | | <h3 class="im-timeline-title"><?= esc_html($c->ms_university) ?></h3> |
| | | <div class="im-timeline-sub">ä¸ä¸ï¼<?= esc_html($c->ms_major ?: 'â') ?></div> |
| | | </div> |
| | | <?php endif; ?> |
| | | <?php if ($c->ca_highschool): ?> |
| | | <div class="im-timeline-item"> |
| | | <div class="im-timeline-icon" style="border-color:#f59e0b"></div> |
| | | <div class="im-timeline-date" style="color:#f59e0b">Canadian High School</div> |
| | | <h3 class="im-timeline-title"><?= esc_html($c->ca_highschool_name ?: 'â') ?></h3> |
| | | <div class="im-timeline-sub">e.g. OSSD or BC curriculum</div> |
| | | </div> |
| | | <?php endif; ?> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- æè½ä¸ç»éª --> |
| | | <div class="im-card-modern"> |
| | | <h2 class="im-card-title"> |
| | | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| | | <path |
| | | d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" /> |
| | | </svg> |
| | | æè½ä¸ç»éª |
| | | </h2> |
| | | <div class="im-info-grid"> |
| | | <div class="im-info-item full-width"> |
| | | <div class="im-info-label">æè¯¾ç§ç® (<?= count($subjects) ?>个)</div> |
| | | <div class="im-info-val"> |
| | | <?php if ($subjects): |
| | | foreach ($subjects as $s): ?> |
| | | <span class="im-tag"><?= esc_html($s) ?></span> |
| | | <?php endforeach; else: ?> |
| | | <span class="im-muted">æªéæ©ä»»ä½ç§ç®</span> |
| | | <?php endif; ?> |
| | | </div> |
| | | </div> |
| | | <div class="im-info-item full-width"> |
| | | <div class="im-info-label">è±è¯æµå©åº¦</div> |
| | | <div class="im-info-val"><?php |
| | | $fluency_map = ['Native' => 'Native (æ¯è¯)', 'Fluent' => 'Fluent (æµå©)', 'Basic' => 'Basic (åºç¡)']; |
| | | echo esc_html($fluency_map[$c->languages] ?? ($c->languages ?: 'â')); |
| | | ?></div> |
| | | </div> |
| | | <div class="im-info-item full-width"> |
| | | <div class="im-info-label">æå¦ç»éª</div> |
| | | <div class="im-info-val"><?= nl2br(esc_html($c->teaching_exp ?: 'â')) ?></div> |
| | | </div> |
| | | <?php if ($c->has_achievement): ?> |
| | | <div class="im-info-item full-width"> |
| | | <div class="im-info-label">个人æå°± (<?= esc_html($c->achievement_type) ?>)</div> |
| | | <div class="im-info-val" |
| | | style="background:#f9fafb;padding:12px 16px;border-radius:8px;border:1px solid #f3f4f6"> |
| | | <?= nl2br(esc_html($c->achievement_desc ?: 'â')) ?> |
| | | </div> |
| | | </div> |
| | | <?php endif; ?> |
| | | <?php if ($c->extra_notes): ?> |
| | | <div class="im-info-item full-width"> |
| | | <div class="im-info-label">è¡¥å
说æ</div> |
| | | <div class="im-info-val" |
| | | style="background:#f9fafb;padding:12px 16px;border-radius:8px;border:1px solid #f3f4f6"> |
| | | <?= nl2br(esc_html($c->extra_notes)) ?> |
| | | </div> |
| | | </div> |
| | | <?php endif; ?> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- å³ä¾§åºå --> |
| | | <div class="im-col-side"> |
| | | <!-- ç¶ææä½åº --> |
| | | <?php if ($c->status === 'screening'): ?> |
| | | <div class="im-action-box"> |
| | | <h3>å®¡æ ¸æä½</h3> |
| | | <p>请å³å®æ¯å¦é请该åé人è¿è¡é¢è¯</p> |
| | | <button class="im-btn-primary im-action-btn" data-action="invite" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>"> |
| | | åéé¢è¯é请 |
| | | </button> |
| | | <button class="im-btn-danger im-action-btn" data-action="reject" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>"> |
| | | æç»è¯¥åé人 |
| | | </button> |
| | | <div id="im-res-<?= $c->id ?>" style="margin-top:12px;font-size:14px;font-weight:600"></div> |
| | | </div> |
| | | <?php elseif ($c->status === 'invited'): ?> |
| | | <div class="im-action-box"> |
| | | <h3>çå¾
é¢è¯ä¸</h3> |
| | | <p>妿åéäººæªæ¶å°é®ä»¶å¯éæ°åé</p> |
| | | <button class="im-btn-primary im-action-btn" data-action="invite" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>"> |
| | | éæ°åéé请 |
| | | </button> |
| | | <div id="im-res-<?= $c->id ?>" style="margin-top:12px;font-size:14px;font-weight:600"></div> |
| | | </div> |
| | | <?php elseif ($c->status === 'completed'): ?> |
| | | <div class="im-action-box" style="background:linear-gradient(135deg,#ecfdf5,#d1fae5);border-color:#a7f3d0"> |
| | | <h3 style="color:#047857">é¢è¯å®¡æ¥</h3> |
| | | <p style="color:#059669">è§é¢å·²æäº¤ï¼è¯·æ¥çå¹¶å¨éè¿åå½ç¨</p> |
| | | <div style="display:flex;gap:12px;flex-wrap:wrap"> |
| | | <button class="im-btn-success im-action-btn" data-action="hire" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>" style="flex:1"> |
| | | æ£å¼å½ååé人 |
| | | </button> |
| | | <button class="im-btn-danger im-action-btn" data-action="reject" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>" style="flex:1"> |
| | | æç»è¯¥åé人 |
| | | </button> |
| | | </div> |
| | | <div id="im-res-<?= $c->id ?>" style="margin-top:12px;font-size:14px;font-weight:600"></div> |
| | | </div> |
| | | <?php elseif ($c->status === 'training'): ?> |
| | | <div class="im-action-box" style="background:linear-gradient(135deg,#fff7ed,#ffedd5);border-color:#fed7aa"> |
| | | <h3 style="color:#c2410c">å¹è®è¿è¡ä¸</h3> |
| | | <p style="color:#ea580c">åé人æ£å¨å®æå¹è®è¯¾ç¨</p> |
| | | <?php if (!empty($c->training_opened_at)): ?> |
| | | <div style="font-size:13px;color:#059669;margin-bottom:12px;font-weight:600">â å·²äº <?= date('Y/m/d H:i', strtotime($c->training_opened_at)) ?> æå¼å¹è®é¡µé¢</div> |
| | | <?php else: ?> |
| | | <div style="font-size:13px;color:#9ca3af;margin-bottom:12px">â å°æªæå¼å¹è®é¡µé¢</div> |
| | | <?php endif; ?> |
| | | <button class="im-btn-primary im-action-btn" data-action="resend_training" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>"> |
| | | éæ°åéå¹è®é¾æ¥ |
| | | </button> |
| | | <div id="im-res-<?= $c->id ?>" style="margin-top:12px;font-size:14px;font-weight:600"></div> |
| | | </div> |
| | | <?php elseif ($c->status === 'trained'): ?> |
| | | <div class="im-action-box" style="background:linear-gradient(135deg,#ecfeff,#cffafe);border-color:#a5f3fc"> |
| | | <h3 style="color:#0e7490">å¹è®å·²å®æ</h3> |
| | | <p style="color:#0891b2">åé人已éè¿ææå¹è®æ¨¡å</p> |
| | | <?php if (!empty($c->training_completed_at)): ?> |
| | | <div style="font-size:13px;color:#059669;font-weight:600;margin-bottom:12px">â äº <?= date('Y/m/d H:i', strtotime($c->training_completed_at)) ?> 宿å¹è®</div> |
| | | <?php endif; ?> |
| | | <button class="im-btn-primary im-action-btn" data-action="resend_trained_email" data-id="<?= $c->id ?>" |
| | | data-name="<?= esc_attr($full_name) ?>"> |
| | | éæ°åéè´¦å·é®ä»¶ |
| | | </button> |
| | | <div id="im-res-<?= $c->id ?>" style="margin-top:12px;font-size:14px;font-weight:600"></div> |
| | | </div> |
| | | <?php endif; ?> |
| | | |
| | | <!-- éä»¶å表 --> |
| | | <?php if ($attachments): ?> |
| | | <div class="im-card-modern"> |
| | | <h2 class="im-card-title"> |
| | | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| | | <path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z" /> |
| | | <path d="M13 2v7h7" /> |
| | | </svg> |
| | | éä»¶ææ |
| | | </h2> |
| | | <?php |
| | | $grouped = []; |
| | | foreach ($attachments as $att) |
| | | $grouped[$att->file_type][] = $att; |
| | | $att_groups = ['transcript_files' => 'æç»©å', 'achievement_files' => 'æå°±è¯æ', 'extra_files' => 'è¡¥å
ææ']; |
| | | foreach ($att_groups as $type => $label): |
| | | if (empty($grouped[$type])) |
| | | continue; |
| | | ?> |
| | | <div style="margin-bottom:16px"> |
| | | <div class="im-info-label" style="margin-bottom:8px"><?= $label ?></div> |
| | | <?php foreach ($grouped[$type] as $att): |
| | | $att_url = $upload_dir['baseurl'] . '/im-applications/' . $c->id . '/' . basename($att->file_path); |
| | | ?> |
| | | <div class="im-doc-item"> |
| | | <div class="im-doc-info"> |
| | | <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" /> |
| | | <path d="M14 2v6h6" /> |
| | | <path d="M16 13H8" /> |
| | | <path d="M16 17H8" /> |
| | | <path d="M10 9H8" /> |
| | | </svg> |
| | | <div class="im-doc-name" title="<?= esc_attr($att->file_name) ?>"> |
| | | <?= esc_html($att->file_name) ?> |
| | | </div> |
| | | </div> |
| | | <div class="im-doc-actions"> |
| | | <a href="<?= esc_url($att_url) ?>" target="_blank" class="im-doc-btn" title="卿°æ ç¾é¡µé¢è§"> |
| | | <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" /> |
| | | <circle cx="12" cy="12" r="3" /> |
| | | </svg> |
| | | </a> |
| | | <a href="<?= esc_url($att_url) ?>" download class="im-doc-btn" title="ä¸è½½æä»¶"> |
| | | <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" /> |
| | | </svg> |
| | | </a> |
| | | </div> |
| | | </div> |
| | | <?php endforeach; ?> |
| | | </div> |
| | | <?php endforeach; ?> |
| | | </div> |
| | | <?php endif; ?> |
| | | |
| | | <!-- é¢è¯è®°å½ --> |
| | | <?php if (in_array($c->status, ['invited', 'completed', 'hired', 'rejected', 'training', 'trained'])): ?> |
| | | <div class="im-card-modern"> |
| | | <h2 class="im-card-title"> |
| | | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| | | <path d="M23 7l-7 5 7 5V7z" /> |
| | | <rect x="1" y="5" width="15" height="14" rx="2" ry="2" /> |
| | | </svg> |
| | | é¢è¯è®°å½ |
| | | </h2> |
| | | <?php if ($tokens): |
| | | foreach ($tokens as $tk): |
| | | $used = (bool) $tk->is_used; |
| | | $expired = strtotime($tk->expires_at) < time(); |
| | | if ($used) { |
| | | $bs = '#dcfce7'; |
| | | $bc = '#86efac'; |
| | | $bt = '#166534'; |
| | | $bl = 'å·²æäº¤è§é¢'; |
| | | } elseif ($expired) { |
| | | $bs = '#fee2e2'; |
| | | $bc = '#fca5a5'; |
| | | $bt = '#991b1b'; |
| | | $bl = '龿¥å·²è¿æ'; |
| | | } else { |
| | | $bs = '#dbeafe'; |
| | | $bc = '#93c5fd'; |
| | | $bt = '#1e40af'; |
| | | $bl = 'ææä¸'; |
| | | } |
| | | ?> |
| | | <div class="im-token-box"> |
| | | <div class="im-token-status"> |
| | | <span class="im-badge" |
| | | style="background:<?= $bs ?>;color:<?= $bt ?>;border-color:<?= $bc ?>"><?= $bl ?></span> |
| | | </div> |
| | | <div class="im-token-date"><?= date('Y/m/d H:i', strtotime($tk->created_at)) ?> åé</div> |
| | | <div class="im-token-exp" style="margin-bottom:8px">è³ |
| | | <?= date('m/d H:i', strtotime($tk->expires_at)) ?> 失æ |
| | | </div> |
| | | |
| | | <?php if (!empty($tk->opened_at)): ?> |
| | | <div class="im-token-exp" style="color:#059669;font-weight:600"> |
| | | â äº <?= date('Y/m/d H:i', strtotime($tk->opened_at)) ?> æ¥çé¢ç® <?= $used ? '' : '(å计æ¶è¿è¡ä¸)' ?> |
| | | </div> |
| | | <?php else: ?> |
| | | <div class="im-token-exp" style="color:#f59e0b">â åéäººæªæå¼é¾æ¥</div> |
| | | <?php endif; ?> |
| | | |
| | | <?php if ($used && $tk->video_path && file_exists($tk->video_path)): |
| | | $vurl = $upload_dir['baseurl'] . '/interviews/' . $c->id . '/' . $tk->video_filename; |
| | | $fext = strtolower(pathinfo($tk->video_filename, PATHINFO_EXTENSION)); |
| | | $archive_exts = ['zip', 'rar', '7z', 'gz', 'tar']; |
| | | $fsize = filesize($tk->video_path); |
| | | $fsize_str = $fsize > 1048576 ? round($fsize / 1048576, 1) . ' MB' : round($fsize / 1024, 1) . ' KB'; |
| | | ?> |
| | | <?php if (in_array($fext, $archive_exts, true)): ?> |
| | | <div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:20px 24px;display:flex;align-items:center;gap:16px;margin-top:8px"> |
| | | <div style="width:48px;height:48px;background:#eef2ff;border-radius:10px;display:flex;align-items:center;justify-content:center;flex-shrink:0"> |
| | | <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="2"><path d="M21 8v13H3V3h12l6 5z"/><path d="M14 3v5h6"/></svg> |
| | | </div> |
| | | <div style="flex:1;min-width:0"> |
| | | <div style="font-weight:600;color:#1e293b;font-size:14px;word-break:break-all"><?= esc_html($tk->video_filename) ?></div> |
| | | <div style="color:#64748b;font-size:13px;margin-top:2px"><?= strtoupper($fext) ?> å缩å
· <?= $fsize_str ?></div> |
| | | </div> |
| | | <a href="<?= esc_url($vurl) ?>" download style="display:inline-flex;align-items:center;gap:6px;background:#6366f1;color:#fff;padding:8px 16px;border-radius:8px;font-size:13px;font-weight:600;text-decoration:none;white-space:nowrap"> |
| | | <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>ä¸è½½ |
| | | </a> |
| | | </div> |
| | | <div style="margin-top:6px;font-size:12px;color:#94a3b8"><?= date('m/d H:i', strtotime($tk->submitted_at)) ?> æäº¤</div> |
| | | <?php else: ?> |
| | | <video controls class="im-video-player" preload="metadata"> |
| | | <source src="<?= esc_url($vurl) ?>"> |
| | | </video> |
| | | <div class="im-video-footer"> |
| | | <span><?= date('m/d H:i', strtotime($tk->submitted_at)) ?> æäº¤</span> |
| | | <a href="<?= esc_url($vurl) ?>" download class="im-video-dl"> |
| | | <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3" /> |
| | | </svg>ä¸è½½ |
| | | </a> |
| | | </div> |
| | | <?php endif; ?> |
| | | <?php elseif ($used): ?> |
| | | <p class="im-muted" style="margin:0;font-size:13px">è§é¢å·²è±æºæä¸åå¨</p> |
| | | <?php endif; ?> |
| | | </div> |
| | | <?php endforeach; else: ?> |
| | | <div class="im-empty" |
| | | style="padding:24px!important;background:#f9fafb;border-radius:12px;border:1px dashed #e5e7eb"> |
| | | å°æªåéé¢è¯é请</div> |
| | | <?php endif; ?> |
| | | </div> |
| | | <?php endif; ?> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <?php im_admin_modal(); ?> |
| | | <?php |
| | | } |
| | | |
| | | /* ============================================================ |
| | | ç¡®è®¤å¼¹çª HTML |
| | | ============================================================ */ |
| | | function im_admin_modal() |
| | | { ?> |
| | | <div id="im-modal" style="display:none" class="im-modal-bg" |
| | | data-nonce="<?php echo esc_attr(wp_create_nonce('im_admin_nonce')); ?>" |
| | | data-ajaxurl="<?php echo esc_url(admin_url('admin-ajax.php')); ?>"> |
| | | <div class="im-modal"> |
| | | <h3>æä½ç¡®è®¤</h3> |
| | | <p id="im-modal-body"></p> |
| | | <textarea id="im-modal-textarea" style="display:none" placeholder="è¾å
¥åå ..."></textarea> |
| | | <div class="im-modal-btns"> |
| | | <button class="button im-modal-cancel">åæ¶</button> |
| | | <button class="button button-primary" id="im-modal-ok">确认</button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <?php |
| | | } |
| | | |
| | | /* ============================================================ |
| | | Nginx é
ç½®å导页ï¼å·¥å
·èåï¼ |
| | | ============================================================ */ |
| | | function im_server_setup() |
| | | { |
| | | $upload_dir = wp_upload_dir(); |
| | | $home = rtrim(home_url('/'), '/'); |
| | | |
| | | $checks = [ |
| | | ['upload_max_filesize', ini_get('upload_max_filesize'), wp_convert_hr_to_bytes(ini_get('upload_max_filesize')) >= 400 * 1024 * 1024, '建议设为 500M'], |
| | | ['post_max_size', ini_get('post_max_size'), wp_convert_hr_to_bytes(ini_get('post_max_size')) >= 400 * 1024 * 1024, '建议设为 512M'], |
| | | ['max_execution_time', ini_get('max_execution_time') . 's', (int) ini_get('max_execution_time') === 0 || (int) ini_get('max_execution_time') >= 180, '建议设为 300'], |
| | | ['memory_limit', ini_get('memory_limit'), true, 'â'], |
| | | ]; |
| | | ?> |
| | | <div class="wrap" style="max-width:860px"> |
| | | <h1>Interview Manager â æå¡å¨é
ç½®å导</h1> |
| | | |
| | | <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:24px"> |
| | | <h2 style="margin-top:0">â Nginx é
ç½®ï¼å¿
é¡»æå¨å®æï¼</h2> |
| | | <p>å¨ <code>server { }</code> åå
æ·»å 以ä¸é
ç½®ï¼ç¶åæ§è¡ <code>sudo nginx -t && sudo nginx -s reload</code>ï¼</p> |
| | | <pre |
| | | style="background:#1e293b;color:#e2e8f0;padding:20px;border-radius:8px;font-size:13px;line-height:1.7;overflow-x:auto"># è§é¢ä¸ä¼ éå¶ |
| | | client_max_body_size 512M; |
| | | client_body_timeout 300s; |
| | | send_timeout 300s; |
| | | |
| | | # WordPress åºå®é¾æ¥ï¼å·²æåè·³è¿ï¼ |
| | | location / { |
| | | try_files $uri $uri/ /index.php?$args; |
| | | } |
| | | |
| | | # ä¸ä¼ ç®å½å®å
¨ |
| | | location ~* ^/wp-content/uploads/(interviews|im-applications)/ { |
| | | add_header X-Content-Type-Options nosniff; |
| | | }</pre> |
| | | </div> |
| | | |
| | | <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:24px"> |
| | | <h2 style="margin-top:0">â¡ PHP é
ç½®ï¼php.ini æ .user.iniï¼</h2> |
| | | <pre style="background:#1e293b;color:#e2e8f0;padding:20px;border-radius:8px;font-size:13px;line-height:1.7">upload_max_filesize = 500M |
| | | post_max_size = 512M |
| | | max_execution_time = 300 |
| | | max_input_time = 300 |
| | | memory_limit = 256M</pre> |
| | | <p>ä¿®æ¹åéå¯ PHP-FPMï¼<code>sudo systemctl restart php8.x-fpm</code></p> |
| | | </div> |
| | | |
| | | <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:24px"> |
| | | <h2 style="margin-top:0">â¢ å·æ° WordPress åºå®é¾æ¥</h2> |
| | | <p>è¿å
¥ <strong>åå° â 设置 â åºå®é¾æ¥</strong>ï¼ç´æ¥ç¹å»ãä¿åæ´æ¹ãå³å¯ï¼æ 鿹任ä½è®¾ç½®ï¼ã</p> |
| | | <p><a href="<?= admin_url('options-permalink.php') ?>" class="button button-primary" target="_blank">åå¾åºå®é¾æ¥è®¾ç½® |
| | | â</a></p> |
| | | </div> |
| | | |
| | | <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px;margin-bottom:24px"> |
| | | <h2 style="margin-top:0">⣠WordPress 页é¢é
ç½®</h2> |
| | | <p>éå¨ WordPress åå°å建以ä¸ä¸¤ä¸ªé¡µé¢ï¼</p> |
| | | <table class="widefat" style="max-width:600px"> |
| | | <tr> |
| | | <th>页é¢</th> |
| | | <th>Shortcode</th> |
| | | <th>建议 URL slug</th> |
| | | </tr> |
| | | <tr> |
| | | <td>Join Us æ¥å页</td> |
| | | <td><code>[im_joinus_form]</code></td> |
| | | <td><code>/join-us/</code></td> |
| | | </tr> |
| | | <tr> |
| | | <td>详ç»ç³è¯·è¡¨å页</td> |
| | | <td><code>[im_apply_form]</code></td> |
| | | <td><code>/apply/</code></td> |
| | | </tr> |
| | | <tr> |
| | | <td>é¢è¯é¡µ</td> |
| | | <td><code>[im_interview]</code></td> |
| | | <td><code>/interview/</code></td> |
| | | </tr> |
| | | <tr> |
| | | <td>å¹è®é¡µ</td> |
| | | <td><code>[im_training]</code></td> |
| | | <td><code>/training/</code></td> |
| | | </tr> |
| | | </table> |
| | | <p style="margin-top:12px">å建好页é¢åï¼å¨ç段 1 é¡¶é¨å°å¯¹åºå¸¸éæ¹ä¸ºå®é
URLï¼</p> |
| | | <code>define('IM_APPLY_PAGE_URL', home_url('/apply/'));</code><br> |
| | | <code>define('IM_INTERVIEW_PAGE_URL', home_url('/interview/'));</code><br> |
| | | <code>define('IM_TRAINING_PAGE_URL', home_url('/training/'));</code><br> |
| | | <code>define('IM_TRAINING_ACCOUNT', 'your_account@example.com');</code><br> |
| | | <code>define('IM_TRAINING_PASSWORD', 'your_password');</code> |
| | | </div> |
| | | |
| | | <div style="background:#fff;border:1px solid #e5e7eb;border-radius:10px;padding:24px"> |
| | | <h2 style="margin-top:0">⤠å½åç¯å¢æ£æµ</h2> |
| | | <table class="widefat" style="max-width:640px"> |
| | | <thead> |
| | | <tr> |
| | | <th>é
置项</th> |
| | | <th>å½åå¼</th> |
| | | <th>ç¶æ</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <?php foreach ($checks as [$name, $val, $ok, $tip]): ?> |
| | | <tr> |
| | | <td><code><?= $name ?></code></td> |
| | | <td><?= esc_html($val) ?></td> |
| | | <td><?= $ok ? 'â
' : 'â ï¸ ' . $tip ?></td> |
| | | </tr> |
| | | <?php endforeach; ?> |
| | | <tr> |
| | | <td>è§é¢åå¨ç®å½</td> |
| | | <td style="font-size:12px"><?= esc_html($upload_dir['basedir'] . '/interviews/') ?></td> |
| | | <td><?= is_dir($upload_dir['basedir'] . '/interviews') ? (is_writable($upload_dir['basedir'] . '/interviews') ? 'â
å¯å' : 'â ä¸å¯å') : 'ï¼é¦æ¬¡ä¸ä¼ æ¶èªå¨å建ï¼' ?> |
| | | </td> |
| | | </tr> |
| | | <tr> |
| | | <td>ç³è¯·æä»¶ç®å½</td> |
| | | <td style="font-size:12px"><?= esc_html($upload_dir['basedir'] . '/im-applications/') ?></td> |
| | | <td><?= is_dir($upload_dir['basedir'] . '/im-applications') ? (is_writable($upload_dir['basedir'] . '/im-applications') ? 'â
å¯å' : 'â ä¸å¯å') : 'ï¼é¦æ¬¡ä¸ä¼ æ¶èªå¨å建ï¼' ?> |
| | | </td> |
| | | </tr> |
| | | <tr> |
| | | <td>é¢è¯é¡µé¢ URL</td> |
| | | <td><a href="<?= esc_url(IM_INTERVIEW_PAGE_URL) ?>" target="_blank" |
| | | style="font-size:12px"><?= esc_html(IM_INTERVIEW_PAGE_URL) ?></a></td> |
| | | <td>â</td> |
| | | </tr> |
| | | <tr> |
| | | <td>å¹è®é¡µé¢ URL</td> |
| | | <td><a href="<?= esc_url(IM_TRAINING_PAGE_URL) ?>" target="_blank" |
| | | style="font-size:12px"><?= esc_html(IM_TRAINING_PAGE_URL) ?></a></td> |
| | | <td>â</td> |
| | | </tr> |
| | | <tr> |
| | | <td>å¹è®ä¸´æ¶è´¦å·</td> |
| | | <td style="font-size:12px"><?= esc_html(IM_TRAINING_ACCOUNT) ?></td> |
| | | <td><?= IM_TRAINING_ACCOUNT !== 'your_account@example.com' ? 'â
' : 'â ï¸ è¯·é
ç½®å®é
è´¦å·' ?></td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | </div> |
| | | <?php |
| | | } |