index.vue 74 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100
  1. <template>
  2. <div class="app-container">
  3. <el-card class="box-card">
  4. <!-- 顶部选项卡 -->
  5. <el-tabs v-model="activeTab" type="border-card" @tab-click="handleTabClick">
  6. <el-tab-pane label="招聘公告" name="recruitment">
  7. <div class="tab-content">
  8. <!-- 搜索区域 -->
  9. <transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
  10. <div v-show="showSearch" class="mb-[10px]">
  11. <el-card shadow="hover">
  12. <el-form ref="queryForm" :model="queryParams" label-width="100px">
  13. <el-row :gutter="20">
  14. <el-col :span="6">
  15. <el-form-item label="职位名称" prop="title">
  16. <el-input v-model="queryParams.title" placeholder="请输入职位名称" clearable @keyup.enter="handleQuery" />
  17. </el-form-item>
  18. </el-col>
  19. <el-col :span="6">
  20. <el-form-item label="部门" prop="department">
  21. <el-select v-model="queryParams.department" placeholder="请选择部门" clearable style="width: 100%">
  22. <el-option v-for="dict in departmentOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
  23. </el-select>
  24. </el-form-item>
  25. </el-col>
  26. <el-col :span="6">
  27. <el-form-item label="状态" prop="isActive">
  28. <el-select v-model="queryParams.isActive" placeholder="请选择状态" clearable style="width: 100%">
  29. <el-option label="招聘中" value="0" />
  30. <el-option label="已停止" value="1" />
  31. </el-select>
  32. </el-form-item>
  33. </el-col>
  34. </el-row>
  35. <el-row>
  36. <el-col :span="24" class="search-btn-container">
  37. <el-form-item>
  38. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  39. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  40. </el-form-item>
  41. </el-col>
  42. </el-row>
  43. </el-form>
  44. </el-card>
  45. </div>
  46. </transition>
  47. <!-- 操作按钮区域 -->
  48. <el-card shadow="never" class="button-card table-card">
  49. <template #header>
  50. <el-row :gutter="10" class="mb8">
  51. <el-col :span="1.5">
  52. <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['rs:job:add']">新增</el-button>
  53. </el-col>
  54. <el-col :span="1.5">
  55. <el-button icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
  56. </el-col>
  57. <el-col :span="1.5">
  58. <el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['rs:job:remove']">删除</el-button>
  59. </el-col>
  60. <el-col :span="1.5">
  61. <el-button icon="Download" @click="handleExport" v-hasPermi="['rs:job:export']" style="display: none">导出</el-button>
  62. </el-col>
  63. <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
  64. </el-row>
  65. </template>
  66. <!-- 表格区域 -->
  67. <el-table v-loading="loading" border :data="jobList" @selection-change="handleSelectionChange" @row-dblclick="handleView">
  68. <el-table-column type="selection" width="55" align="center" />
  69. <el-table-column label="职位名称" align="center" prop="title" :show-overflow-tooltip="true" />
  70. <el-table-column label="部门" align="center" prop="department">
  71. <template #default="scope">
  72. <el-tag type="info">{{ getDepartmentLabel(scope.row.department) }}</el-tag>
  73. </template>
  74. </el-table-column>
  75. <el-table-column label="工作地点" align="center" prop="location" />
  76. <el-table-column label="状态" align="center" prop="isActive" width="120">
  77. <template #default="scope">
  78. <el-tag :type="scope.row.isActive === '0' ? 'success' : 'info'">
  79. {{ scope.row.isActive === '0' ? '招聘中' : '已停止' }}
  80. </el-tag>
  81. </template>
  82. </el-table-column>
  83. <el-table-column label="申请人数" align="center" prop="applicantCount" width="100" />
  84. <el-table-column label="浏览次数" align="center" prop="viewCount" width="100" />
  85. <el-table-column label="创建时间" align="center" prop="createTime" width="180">
  86. <template #default="scope">
  87. <span>{{ parseTime(scope.row.createTime) }}</span>
  88. </template>
  89. </el-table-column>
  90. <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="280">
  91. <template #default="scope">
  92. <el-tooltip content="查看" placement="top">
  93. <el-button
  94. link
  95. type="primary"
  96. icon="View"
  97. size="default"
  98. style="font-size: 16px; margin-right: 8px; color: blue"
  99. @click="handleView(scope.row)"
  100. ></el-button>
  101. </el-tooltip>
  102. <el-tooltip content="修改" placement="top">
  103. <el-button
  104. link
  105. type="primary"
  106. icon="Edit"
  107. size="default"
  108. style="font-size: 16px; margin-right: 8px"
  109. @click="handleUpdate(scope.row)"
  110. ></el-button>
  111. </el-tooltip>
  112. <el-tooltip content="查看简历记录" placement="top">
  113. <el-button
  114. link
  115. type="info"
  116. icon="Document"
  117. size="default"
  118. style="font-size: 16px; margin-right: 8px"
  119. @click="handleApplications(scope.row)"
  120. v-if="scope.row.jobId"
  121. ></el-button>
  122. </el-tooltip>
  123. <el-tooltip content="删除" placement="top">
  124. <el-button
  125. link
  126. type="danger"
  127. icon="Delete"
  128. size="default"
  129. style="font-size: 16px; margin-right: 8px"
  130. @click="handleDelete(scope.row)"
  131. v-hasPermi="['rs:job:remove']"
  132. ></el-button>
  133. </el-tooltip>
  134. <el-tooltip :content="scope.row.isActive === '0' ? '停止招聘' : '开始招聘'" placement="top">
  135. <el-button
  136. link
  137. :type="scope.row.isActive === '0' ? 'warning' : 'success'"
  138. :icon="scope.row.isActive === '0' ? 'CircleClose' : 'VideoPlay'"
  139. size="default"
  140. style="font-size: 16px"
  141. @click="handleToggleStatus(scope.row)"
  142. ></el-button>
  143. </el-tooltip>
  144. </template>
  145. </el-table-column>
  146. </el-table>
  147. <div class="pagination-container">
  148. <pagination
  149. v-show="total > 0"
  150. :total="total"
  151. v-model:page="queryParams.pageNum"
  152. v-model:limit="queryParams.pageSize"
  153. @pagination="getList"
  154. />
  155. </div>
  156. </el-card>
  157. </div>
  158. </el-tab-pane>
  159. <el-tab-pane label="简历管理" name="application">
  160. <div class="tab-content">
  161. <!-- 搜索区域 -->
  162. <transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
  163. <div v-show="showApplicationSearch" class="mb-[10px]">
  164. <el-card shadow="hover">
  165. <el-form ref="applicationQueryForm" :model="applicationQueryParams" label-width="100px">
  166. <el-row :gutter="20">
  167. <el-col :span="6">
  168. <el-form-item label="职位名称" prop="jobTitle">
  169. <el-input
  170. v-model="applicationQueryParams.jobTitle"
  171. placeholder="请输入职位名称"
  172. clearable
  173. @keyup.enter="handleApplicationQuery"
  174. />
  175. </el-form-item>
  176. </el-col>
  177. <el-col :span="6">
  178. <el-form-item label="应聘者" prop="applicantName">
  179. <el-input
  180. v-model="applicationQueryParams.applicantName"
  181. placeholder="请输入应聘者姓名"
  182. clearable
  183. @keyup.enter="handleApplicationQuery"
  184. />
  185. </el-form-item>
  186. </el-col>
  187. <el-col :span="6">
  188. <el-form-item label="申请状态" prop="status">
  189. <el-select v-model="applicationQueryParams.status" placeholder="请选择申请状态" clearable style="width: 100%">
  190. <el-option v-for="dict in applicationStatusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
  191. </el-select>
  192. </el-form-item>
  193. </el-col>
  194. <el-col :span="6">
  195. <el-form-item label="联系电话" prop="contactPhone">
  196. <el-input
  197. v-model="applicationQueryParams.contactPhone"
  198. placeholder="请输入联系电话"
  199. clearable
  200. @keyup.enter="handleApplicationQuery"
  201. />
  202. </el-form-item>
  203. </el-col>
  204. </el-row>
  205. <el-row>
  206. <el-col :span="24" class="search-btn-container">
  207. <el-form-item>
  208. <el-button type="primary" icon="Search" @click="handleApplicationQuery">搜索</el-button>
  209. <el-button icon="Refresh" @click="resetApplicationQuery">重置</el-button>
  210. </el-form-item>
  211. </el-col>
  212. </el-row>
  213. </el-form>
  214. </el-card>
  215. </div>
  216. </transition>
  217. <!-- 操作按钮区域 -->
  218. <el-card shadow="never" class="table-card">
  219. <template #header>
  220. <el-row :gutter="10" class="mb8">
  221. <el-col :span="1.5">
  222. <el-button icon="Download" @click="handleExportApplications" v-hasPermi="['rs:application:export']" style="display: none"
  223. >导出</el-button
  224. >
  225. </el-col>
  226. <el-col :span="1.5">
  227. <el-button icon="Printer" @click="handlePrintApplications" style="display: none">打印</el-button>
  228. </el-col>
  229. <el-col :span="1.5">
  230. <el-button icon="Document" :disabled="applicationIds.length !== 1" @click="handleExportResume">导出简历</el-button>
  231. </el-col>
  232. <right-toolbar v-model:showSearch="showApplicationSearch" @queryTable="getApplicationList"></right-toolbar>
  233. </el-row>
  234. </template>
  235. <!-- 表格区域 -->
  236. <el-table
  237. v-loading="applicationLoading"
  238. border
  239. :data="applicationList"
  240. @selection-change="handleApplicationSelectionChange"
  241. @row-dblclick="handleViewApplicant"
  242. >
  243. <el-table-column type="selection" width="55" align="center" />
  244. <el-table-column label="职位名称" align="center" prop="jobTitle" :show-overflow-tooltip="true" />
  245. <el-table-column label="应聘者" align="center" prop="applicantName" />
  246. <el-table-column label="联系电话" align="center" prop="contactPhone" />
  247. <el-table-column label="联系邮箱" align="center" prop="contactEmail" width="200" :show-overflow-tooltip="true" />
  248. <el-table-column label="申请时间" align="center" prop="applyTime" width="180">
  249. <template #default="scope">
  250. <span>{{ parseTime(scope.row.applyTime) }}</span>
  251. </template>
  252. </el-table-column>
  253. <el-table-column label="申请状态" align="center" prop="status" width="120">
  254. <template #default="scope">
  255. <el-tag :type="getStatusType(scope.row.status)">
  256. {{ getStatusLabel(scope.row.status) }}
  257. </el-tag>
  258. </template>
  259. </el-table-column>
  260. <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="300">
  261. <template #default="scope">
  262. <div class="action-buttons">
  263. <el-tooltip content="查看详情" placement="top">
  264. <el-button link type="primary" icon="View" size="small" @click="handleViewApplicant(scope.row)"></el-button>
  265. </el-tooltip>
  266. <el-tooltip content="下载简历" placement="top" v-if="scope.row.resumePath">
  267. <el-button link type="primary" icon="Download" size="small" @click="downloadResume(scope.row.resumePath)"></el-button>
  268. </el-tooltip>
  269. <el-tooltip content="修改" placement="top">
  270. <el-button
  271. link
  272. type="warning"
  273. icon="Edit"
  274. size="small"
  275. @click="handleEditApplicant(scope.row)"
  276. v-hasPermi="['rs:application:edit']"
  277. ></el-button>
  278. </el-tooltip>
  279. </div>
  280. </template>
  281. </el-table-column>
  282. </el-table>
  283. <div class="pagination-container">
  284. <pagination
  285. v-show="applicationTotal > 0"
  286. :total="applicationTotal"
  287. v-model:page="applicationQueryParams.pageNum"
  288. v-model:limit="applicationQueryParams.pageSize"
  289. @pagination="getApplicationList"
  290. />
  291. </div>
  292. </el-card>
  293. </div>
  294. </el-tab-pane>
  295. </el-tabs>
  296. </el-card>
  297. <!-- 添加或修改职位对话框 -->
  298. <el-dialog :title="title" v-model="open" width="700px" append-to-body>
  299. <el-form ref="jobForm" :model="form" :rules="rules" label-width="100px">
  300. <el-form-item label="职位名称" prop="title">
  301. <el-input v-model="form.title" placeholder="请输入职位名称" />
  302. </el-form-item>
  303. <el-form-item label="部门" prop="department">
  304. <el-select v-model="form.department" placeholder="请选择部门" style="width: 100%">
  305. <el-option v-for="dict in departmentOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
  306. </el-select>
  307. </el-form-item>
  308. <el-form-item label="工作地点" prop="location">
  309. <el-input v-model="form.location" placeholder="请输入工作地点" />
  310. </el-form-item>
  311. <el-form-item label="发布时间" prop="publishTime">
  312. <el-date-picker
  313. v-model="form.publishTime"
  314. type="datetime"
  315. placeholder="选择发布时间"
  316. style="width: 100%"
  317. value-format="YYYY-MM-DD HH:mm:ss"
  318. />
  319. </el-form-item>
  320. <el-form-item label="职位描述" prop="description">
  321. <el-input
  322. v-model="form.description"
  323. type="textarea"
  324. placeholder="请输入职位描述"
  325. resize="none"
  326. style="height: 150px"
  327. :autosize="false"
  328. :rows="6"
  329. />
  330. </el-form-item>
  331. <el-form-item label="任职要求" prop="requirements">
  332. <el-input
  333. v-model="form.requirements"
  334. type="textarea"
  335. placeholder="请输入任职要求"
  336. resize="none"
  337. style="height: 150px"
  338. :autosize="false"
  339. :rows="6"
  340. />
  341. </el-form-item>
  342. <el-form-item label="状态" prop="isActive">
  343. <el-radio-group v-model="form.isActive">
  344. <el-radio label="0">招聘中</el-radio>
  345. <el-radio label="1">已停止</el-radio>
  346. </el-radio-group>
  347. </el-form-item>
  348. <el-form-item label="备注" prop="remark">
  349. <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
  350. </el-form-item>
  351. </el-form>
  352. <template #footer>
  353. <div class="dialog-footer">
  354. <el-button :loading="buttonLoading" plain type="primary" @click="submitForm">确 定</el-button>
  355. <el-button @click="cancel">取 消</el-button>
  356. </div>
  357. </template>
  358. </el-dialog>
  359. <!-- 查看职位详情对话框 -->
  360. <el-dialog title="职位详情" v-model="viewOpen" width="700px" append-to-body>
  361. <el-descriptions :column="2" border>
  362. <el-descriptions-item label="职位名称">{{ viewForm.title }}</el-descriptions-item>
  363. <el-descriptions-item label="部门">{{ getDepartmentLabel(viewForm.department) }}</el-descriptions-item>
  364. <el-descriptions-item label="工作地点">{{ viewForm.location }}</el-descriptions-item>
  365. <el-descriptions-item label="状态">
  366. <el-tag :type="viewForm.isActive === '0' ? 'success' : 'info'">
  367. {{ viewForm.isActive === '0' ? '招聘中' : '已停止' }}
  368. </el-tag>
  369. </el-descriptions-item>
  370. <el-descriptions-item label="申请人数">{{ viewForm.applicantCount }}</el-descriptions-item>
  371. <el-descriptions-item label="浏览次数">{{ viewForm.viewCount }}</el-descriptions-item>
  372. <el-descriptions-item label="应聘链接">
  373. <el-link
  374. :href="`/recruitment-apply?jobId=${viewForm.jobId}`"
  375. target="_blank"
  376. type="primary"
  377. :underline="false"
  378. style="font-size: 14px;"
  379. >
  380. 点击前往应聘页面
  381. <el-icon style="margin-left: 4px;"><Link /></el-icon>
  382. </el-link>
  383. </el-descriptions-item>
  384. <el-descriptions-item label="发布时间">
  385. {{ parseTime(viewForm.publishTime) || (viewForm.isActive === '0' ? parseTime(viewForm.createTime) || '长期有效' : '未发布') }}
  386. </el-descriptions-item>
  387. <el-descriptions-item label="创建时间">{{ parseTime(viewForm.createTime) }}</el-descriptions-item>
  388. <el-descriptions-item label="职位描述" :span="2">
  389. <div class="view-description">{{ viewForm.description || '暂无描述' }}</div>
  390. </el-descriptions-item>
  391. <el-descriptions-item label="任职要求" :span="2">
  392. <div class="view-description">{{ viewForm.requirements || '暂无要求' }}</div>
  393. </el-descriptions-item>
  394. <el-descriptions-item label="备注" :span="2">{{ viewForm.remark || '暂无备注' }}</el-descriptions-item>
  395. </el-descriptions>
  396. <template #footer>
  397. <div class="dialog-footer">
  398. <el-button @click="viewOpen = false">关 闭</el-button>
  399. </div>
  400. </template>
  401. </el-dialog>
  402. <!-- 简历记录对话框 -->
  403. <el-dialog title="简历记录" v-model="applicationOpen" width="1200px" append-to-body>
  404. <div v-if="currentJob">
  405. <div class="job-info-header">
  406. <div class="job-info-compact">
  407. <div class="info-item">
  408. <span class="info-label">职位名称:</span>
  409. <span class="info-value">{{ currentJob.title }}</span>
  410. </div>
  411. <div class="info-divider">|</div>
  412. <div class="info-item">
  413. <span class="info-label">部门:</span>
  414. <span class="info-value">{{ getDepartmentLabel(currentJob.department) }}</span>
  415. </div>
  416. <div class="info-divider">|</div>
  417. <div class="info-item">
  418. <span class="info-label">申请人数:</span>
  419. <span class="info-value highlight">{{ currentJob.applicantCount }}</span>
  420. </div>
  421. </div>
  422. </div>
  423. <div class="application-table mt-4">
  424. <el-table
  425. v-loading="applicationLoading"
  426. :data="applicationList"
  427. border
  428. size="small"
  429. class="compact-table"
  430. :row-style="{ height: '32px' }"
  431. :cell-style="{ padding: '2px 0' }"
  432. >
  433. <el-table-column label="应聘者" align="center" prop="applicantName" width="110" />
  434. <el-table-column label="申请状态" align="center" prop="status" width="110">
  435. <template #default="scope">
  436. <el-tag :type="getStatusType(scope.row.status)" size="small">
  437. {{ getStatusLabel(scope.row.status) }}
  438. </el-tag>
  439. </template>
  440. </el-table-column>
  441. <el-table-column label="联系电话" align="center" prop="contactPhone" width="130" />
  442. <el-table-column label="联系邮箱" align="center" prop="contactEmail" width="200" :show-overflow-tooltip="true" />
  443. <el-table-column label="自我介绍" align="center" prop="selfDescription" width="180" :show-overflow-tooltip="true" />
  444. <el-table-column label="申请时间" align="center" prop="applyTime" width="170">
  445. <template #default="scope">
  446. <span>{{ parseTime(scope.row.applyTime) }}</span>
  447. </template>
  448. </el-table-column>
  449. <el-table-column label="HR备注" align="center" prop="hrNotes" width="150" :show-overflow-tooltip="true" />
  450. <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="130" fixed="right">
  451. <template #default="scope">
  452. <div class="action-buttons">
  453. <el-tooltip content="查看详情" placement="top">
  454. <el-button link type="primary" icon="View" size="small" @click="handleViewApplicant(scope.row)"></el-button>
  455. </el-tooltip>
  456. <el-tooltip content="下载简历" placement="top" v-if="scope.row.resumePath">
  457. <el-button link type="primary" icon="Download" size="small" @click="downloadResume(scope.row.resumePath)"></el-button>
  458. </el-tooltip>
  459. <!-- 状态选择器已隐藏 -->
  460. <!-- <div class="status-selector" v-hasPermi="['rs:application:edit']">
  461. <el-select
  462. :model-value="scope.row.status"
  463. @change="(newStatus: string) => handleStatusChange(scope.row, newStatus)"
  464. size="small"
  465. style="width: 100px"
  466. >
  467. <el-option v-for="option in applicationStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
  468. </el-select>
  469. </div> -->
  470. </div>
  471. </template>
  472. </el-table-column>
  473. </el-table>
  474. <div class="pagination-container">
  475. <pagination
  476. v-show="applicationTotal > 0"
  477. :total="applicationTotal"
  478. v-model:page="applicationQueryParams.pageNum"
  479. v-model:limit="applicationQueryParams.pageSize"
  480. @pagination="getApplicationList"
  481. :page-sizes="[5, 10, 15, 20]"
  482. layout="sizes, prev, pager, next, jumper"
  483. :small="true"
  484. />
  485. </div>
  486. </div>
  487. </div>
  488. </el-dialog>
  489. <!-- 应聘者详情对话框 -->
  490. <el-dialog v-model="applicationDetailOpen" title="应聘者详情" width="700px" append-to-body>
  491. <div class="applicant-detail">
  492. <el-descriptions :column="2" border>
  493. <el-descriptions-item label="职位名称">{{ applicationDetail.jobTitle }}</el-descriptions-item>
  494. <el-descriptions-item label="应聘者">{{ applicationDetail.applicantName }}</el-descriptions-item>
  495. <el-descriptions-item label="申请状态">
  496. <el-tag :type="applicationDetail.status === '0' ? 'warning' : applicationDetail.status === '1' ? 'success' : 'danger'">
  497. {{ getStatusLabel(applicationDetail.status) }}
  498. </el-tag>
  499. </el-descriptions-item>
  500. <el-descriptions-item label="申请时间">{{ parseTime(applicationDetail.applyTime) }}</el-descriptions-item>
  501. <el-descriptions-item label="联系电话">{{ applicationDetail.contactPhone }}</el-descriptions-item>
  502. <el-descriptions-item label="联系邮箱">
  503. <div style="display: flex; align-items: center; gap: 8px;">
  504. <span>{{ applicationDetail.contactEmail }}</span>
  505. <el-button
  506. v-if="applicationDetail.contactEmail"
  507. size="small"
  508. type="primary"
  509. :icon="Message"
  510. @click="sendEmail(applicationDetail.contactEmail, applicationDetail.applicantName)"
  511. style="margin-left: 8px;"
  512. >
  513. 发送邮件
  514. </el-button>
  515. </div>
  516. </el-descriptions-item>
  517. <el-descriptions-item label="学历">{{ applicationDetail.education || '暂无信息' }}</el-descriptions-item>
  518. <el-descriptions-item label="教育背景">{{ applicationDetail.educationBackground || '暂无信息' }}</el-descriptions-item>
  519. <el-descriptions-item label="工作经验" :span="2">{{ applicationDetail.experience || '暂无信息' }}</el-descriptions-item>
  520. <el-descriptions-item label="自我介绍" :span="2">
  521. <div class="view-description">{{ applicationDetail.selfDescription || '暂无自我介绍' }}</div>
  522. </el-descriptions-item>
  523. <el-descriptions-item label="HR备注" :span="2">
  524. <el-input
  525. v-model="applicationDetail.hrNotes"
  526. type="textarea"
  527. :rows="4"
  528. placeholder="请输入HR备注"
  529. :disabled="applicationDetail.status !== '0'"
  530. />
  531. </el-descriptions-item>
  532. </el-descriptions>
  533. </div>
  534. <template #footer>
  535. <div class="dialog-footer">
  536. <el-button type="primary" @click="saveHrNotes" v-if="applicationDetail.status === '0'">保存备注</el-button>
  537. <el-button type="success" @click="handleUpdateStatus(applicationDetail, '1')" v-if="applicationDetail.status === '0'">通过申请</el-button>
  538. <el-button type="danger" @click="handleUpdateStatus(applicationDetail, '2')" v-if="applicationDetail.status === '0'">拒绝申请</el-button>
  539. <el-button @click="applicationDetailOpen = false">关闭</el-button>
  540. </div>
  541. </template>
  542. </el-dialog>
  543. <!-- 应聘者详情对话框 -->
  544. <el-dialog v-model="applicantViewOpen" title="应聘者详情" width="700px" append-to-body>
  545. <el-descriptions :column="2" border>
  546. <el-descriptions-item label="姓名">{{ currentApplication?.applicantName }}</el-descriptions-item>
  547. <el-descriptions-item label="应聘职位">{{ currentApplication?.jobTitle }}</el-descriptions-item>
  548. <el-descriptions-item label="性别">{{ currentApplication?.gender || '未提供' }}</el-descriptions-item>
  549. <el-descriptions-item label="年龄">{{ currentApplication?.age || '未提供' }}</el-descriptions-item>
  550. <el-descriptions-item label="联系电话">{{ currentApplication?.contactPhone }}</el-descriptions-item>
  551. <el-descriptions-item label="邮箱地址">
  552. <div style="display: flex; align-items: center; gap: 8px;">
  553. <span>{{ currentApplication?.contactEmail }}</span>
  554. <el-button
  555. v-if="currentApplication?.contactEmail"
  556. size="small"
  557. type="primary"
  558. :icon="Message"
  559. @click="sendEmail(currentApplication?.contactEmail, currentApplication?.applicantName)"
  560. style="margin-left: 8px;"
  561. >
  562. 发送邮件
  563. </el-button>
  564. </div>
  565. </el-descriptions-item>
  566. <el-descriptions-item label="申请状态">
  567. <el-tag
  568. :type="
  569. currentApplication?.status === '0'
  570. ? 'warning'
  571. : currentApplication?.status === '1'
  572. ? 'success'
  573. : currentApplication?.status === '2'
  574. ? 'danger'
  575. : 'info'
  576. "
  577. >
  578. {{ getStatusLabel(currentApplication?.status) }}
  579. </el-tag>
  580. </el-descriptions-item>
  581. <el-descriptions-item label="申请时间">{{ parseTime(currentApplication?.applyTime) }}</el-descriptions-item>
  582. <el-descriptions-item label="学历">{{ currentApplication?.education || '未提供' }}</el-descriptions-item>
  583. <el-descriptions-item label="教育背景">{{ currentApplication?.educationBackground || '未提供' }}</el-descriptions-item>
  584. <el-descriptions-item label="工作经验" :span="2">{{ currentApplication?.experience || '未提供' }}</el-descriptions-item>
  585. <el-descriptions-item label="自我介绍" :span="2">
  586. <div class="view-description">{{ currentApplication?.selfDescription || '未提供自我介绍' }}</div>
  587. </el-descriptions-item>
  588. <el-descriptions-item label="HR备注" :span="2">
  589. <div class="view-description">{{ currentApplication?.hrNotes || '暂无备注' }}</div>
  590. </el-descriptions-item>
  591. </el-descriptions>
  592. <template #footer>
  593. <div class="dialog-footer">
  594. <el-button @click="applicantViewOpen = false">关 闭</el-button>
  595. </div>
  596. </template>
  597. </el-dialog>
  598. <!-- 编辑应聘者信息对话框 -->
  599. <el-dialog v-model="applicantEditOpen" title="编辑应聘者信息" width="700px" append-to-body>
  600. <el-form ref="applicantEditFormRef" :model="editForm" :rules="editRules" label-width="100px">
  601. <el-row :gutter="20">
  602. <el-col :span="12">
  603. <el-form-item label="姓名">
  604. <el-input v-model="editForm.applicantName" readonly />
  605. </el-form-item>
  606. </el-col>
  607. <el-col :span="12">
  608. <el-form-item label="应聘职位">
  609. <el-input v-model="editForm.jobTitle" readonly />
  610. </el-form-item>
  611. </el-col>
  612. </el-row>
  613. <el-row :gutter="20">
  614. <el-col :span="12">
  615. <el-form-item label="性别">
  616. <el-input :model-value="editForm.gender || '未提供'" readonly />
  617. </el-form-item>
  618. </el-col>
  619. <el-col :span="12">
  620. <el-form-item label="年龄">
  621. <el-input :model-value="editForm.age || '未提供'" readonly />
  622. </el-form-item>
  623. </el-col>
  624. </el-row>
  625. <el-row :gutter="20">
  626. <el-col :span="12">
  627. <el-form-item label="联系电话">
  628. <el-input v-model="editForm.contactPhone" readonly />
  629. </el-form-item>
  630. </el-col>
  631. <el-col :span="12">
  632. <el-form-item label="邮箱地址">
  633. <el-input v-model="editForm.contactEmail" readonly />
  634. </el-form-item>
  635. </el-col>
  636. </el-row>
  637. <el-row :gutter="20">
  638. <el-col :span="12">
  639. <el-form-item label="学历">
  640. <el-input :model-value="editForm.education || '未提供'" readonly />
  641. </el-form-item>
  642. </el-col>
  643. <el-col :span="12">
  644. <el-form-item label="申请时间">
  645. <el-input :model-value="parseTime(editForm.applyTime)" readonly />
  646. </el-form-item>
  647. </el-col>
  648. </el-row>
  649. <el-form-item label="教育背景">
  650. <el-input :model-value="editForm.educationBackground || '未提供'" type="textarea" :rows="3" readonly placeholder="暂无教育背景" />
  651. </el-form-item>
  652. <el-form-item label="自我介绍">
  653. <el-input v-model="editForm.selfDescription" type="textarea" :rows="3" readonly placeholder="暂无自我介绍" />
  654. </el-form-item>
  655. <el-form-item label="工作经验">
  656. <el-input v-model="editForm.experience" type="textarea" :rows="3" readonly placeholder="暂无工作经验" />
  657. </el-form-item>
  658. <el-form-item label="申请状态" prop="status">
  659. <el-select v-model="editForm.status" placeholder="请选择申请状态" style="width: 100%">
  660. <el-option v-for="option in applicationStatusOptions" :key="option.value" :label="option.label" :value="option.value" />
  661. </el-select>
  662. </el-form-item>
  663. <el-form-item label="HR备注" prop="hrNotes">
  664. <el-input v-model="editForm.hrNotes" type="textarea" :rows="4" placeholder="请输入HR备注..." maxlength="500" show-word-limit />
  665. </el-form-item>
  666. </el-form>
  667. <template #footer>
  668. <div class="dialog-footer">
  669. <el-button :loading="buttonLoading" type="primary" @click="submitEdit">保 存</el-button>
  670. <el-button @click="applicantEditOpen = false">取 消</el-button>
  671. </div>
  672. </template>
  673. </el-dialog>
  674. </div>
  675. </template>
  676. <script setup lang="ts" name="PersonnelManagement">
  677. import { ref, reactive, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
  678. import { ElMessage, ElMessageBox } from 'element-plus';
  679. import {
  680. listJob,
  681. getJob,
  682. addJob,
  683. updateJob,
  684. delJob,
  685. toggleJobStatus,
  686. exportJob,
  687. listApplications,
  688. listAllApplications,
  689. updateApplicationStatus,
  690. updateApplicationInfo,
  691. getApplication,
  692. exportApplication,
  693. uploadAttachment,
  694. exportResumePdf
  695. } from '@/api/rs/job';
  696. import { parseTime } from '@/utils/ruoyi';
  697. import type { JobVO, JobForm, ApplicationVO } from '@/api/rs/job';
  698. import { listDept } from '@/api/system/dept';
  699. import type { DeptVO } from '@/api/system/dept/types';
  700. import { Document, Link, Message } from '@element-plus/icons-vue';
  701. import { getToken } from '@/utils/auth';
  702. import jsPDF from 'jspdf';
  703. import html2canvas from 'html2canvas';
  704. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  705. const animate = proxy?.animate || { searchAnimate: { enter: 'animate__fadeInUp', leave: 'animate__fadeOutDown' } };
  706. // 遮罩层
  707. const loading = ref(true);
  708. const buttonLoading = ref(false);
  709. // 选中数组
  710. const ids = ref<Array<string | number>>([]);
  711. // 非单个禁用
  712. const single = ref(true);
  713. // 非多个禁用
  714. const multiple = ref(true);
  715. // 显示搜索条件
  716. const showSearch = ref(true);
  717. // 总条数
  718. const total = ref(0);
  719. // 职位表格数据
  720. const jobList = ref<JobVO[]>([]);
  721. // 弹出层标题
  722. const title = ref('');
  723. // 是否显示弹出层
  724. const open = ref(false);
  725. // 是否显示详情弹出层
  726. const viewOpen = ref(false);
  727. // 是否显示申请记录弹出层
  728. const applicationOpen = ref(false);
  729. // 当前激活的选项卡
  730. const activeTab = ref('recruitment');
  731. // 部门选项 - 从数据库sys_dept表获取
  732. const departmentOptions = ref<Array<{ value: string; label: string }>>([]);
  733. // 申请状态选项
  734. const applicationStatusOptions = [
  735. { value: 'pending', label: '待处理', type: 'warning' },
  736. { value: 'interviewed', label: '已面试', type: 'primary' },
  737. { value: 'offered', label: '已通过', type: 'success' },
  738. { value: 'eliminated', label: '已淘汰', type: 'info' },
  739. { value: 'rejected', label: '已拒绝', type: 'danger' }
  740. ];
  741. // 查询参数
  742. const queryParams = reactive({
  743. pageNum: 1,
  744. pageSize: 10,
  745. title: '',
  746. department: '',
  747. isActive: '',
  748. params: {}
  749. });
  750. // 表单参数
  751. const form = reactive<JobForm>({
  752. jobId: undefined,
  753. title: '',
  754. department: '',
  755. salaryRange: '',
  756. location: '',
  757. description: '',
  758. requirements: '',
  759. isActive: '0',
  760. publishTime: '',
  761. remark: ''
  762. });
  763. // 查看表单
  764. const viewForm = reactive<JobVO>({
  765. jobId: undefined,
  766. title: '',
  767. department: '',
  768. salaryRange: '',
  769. location: '',
  770. description: '',
  771. requirements: '',
  772. isActive: '0',
  773. applicantCount: 0,
  774. viewCount: 0,
  775. publishTime: '',
  776. createTime: '',
  777. updateTime: '',
  778. createBy: '',
  779. updateBy: '',
  780. remark: ''
  781. });
  782. // 表单校验
  783. const rules = reactive<Record<string, any>>({
  784. title: [
  785. { required: true, message: '职位名称不能为空', trigger: 'blur' },
  786. { min: 2 as number, max: 50 as number, message: '长度在 2 到 50 个字符', trigger: 'blur' }
  787. ],
  788. department: [{ required: true, message: '部门不能为空', trigger: 'change' }],
  789. location: [{ required: true, message: '工作地点不能为空', trigger: 'blur' }]
  790. });
  791. // 培训申请表单校验规则
  792. const applicationRules = reactive({
  793. reason: [
  794. { required: true, message: '申请理由不能为空', trigger: 'blur' },
  795. { min: 10, max: 500, message: '长度在 10 到 500 个字符', trigger: 'blur' }
  796. ]
  797. });
  798. // 表单引用
  799. const jobForm = ref<any>(null);
  800. const queryForm = ref<any>(null);
  801. // 申请记录相关
  802. const applicationLoading = ref(false);
  803. const applicationList = ref<ApplicationVO[]>([]);
  804. const applicationTotal = ref(0);
  805. const currentJob = ref<JobVO | null>(null);
  806. const applicationQueryParams = reactive({
  807. pageNum: 1,
  808. pageSize: 10,
  809. jobId: undefined as string | number | undefined,
  810. jobTitle: '',
  811. applicantName: '',
  812. contactPhone: '',
  813. status: '',
  814. params: {}
  815. });
  816. // 申请管理页面相关
  817. const showApplicationSearch = ref(true);
  818. const applicationIds = ref<Array<string | number>>([]);
  819. const applicationSingle = ref(true);
  820. const applicationMultiple = ref(true);
  821. // 已有申请管理相关变量,不需要重复定义
  822. // 申请详情对话框
  823. const applicationDetailOpen = ref(false);
  824. const applicantViewOpen = ref(false); // 应聘者详情对话框
  825. const applicantEditOpen = ref(false); // 编辑应聘者信息对话框
  826. const currentApplication = ref<ApplicationVO | null>(null); // 当前查看的应聘者
  827. // 编辑表单
  828. const editForm = reactive<ApplicationVO>({
  829. applicationId: 0,
  830. jobId: 0,
  831. jobTitle: '',
  832. applicantId: 0,
  833. applicantName: '',
  834. gender: '', // 新增:性别
  835. age: '', // 新增:年龄
  836. status: '',
  837. applyTime: '',
  838. resumePath: '',
  839. selfDescription: '',
  840. hrNotes: '',
  841. contactPhone: '',
  842. contactEmail: '',
  843. education: '',
  844. educationBackground: '', // 新增:教育背景
  845. experience: '',
  846. remark: '',
  847. createBy: '',
  848. createTime: '',
  849. updateBy: '',
  850. updateTime: ''
  851. });
  852. // 编辑表单校验规则
  853. const editRules = reactive({
  854. status: [{ required: true, message: '请选择申请状态', trigger: 'change' }]
  855. });
  856. const applicationDetail = reactive<ApplicationVO>({
  857. applicationId: 0,
  858. jobId: 0,
  859. jobTitle: '',
  860. applicantId: 0,
  861. applicantName: '',
  862. gender: '', // 新增:性别
  863. age: '', // 新增:年龄
  864. status: '',
  865. applyTime: '',
  866. resumePath: '',
  867. selfDescription: '',
  868. hrNotes: '',
  869. contactPhone: '',
  870. contactEmail: '',
  871. education: '',
  872. educationBackground: '', // 新增:教育背景
  873. experience: '',
  874. remark: '',
  875. createBy: '',
  876. createTime: '',
  877. updateBy: '',
  878. updateTime: ''
  879. });
  880. /** 查询职位列表 */
  881. function getList() {
  882. loading.value = true;
  883. listJob(queryParams)
  884. .then((response) => {
  885. jobList.value = response.rows || [];
  886. total.value = response.total || 0;
  887. loading.value = false;
  888. })
  889. .catch((error) => {
  890. console.error('获取职位列表失败:', error);
  891. // 如果API调用失败,可以使用模拟数据
  892. mockJobData();
  893. loading.value = false;
  894. });
  895. }
  896. /** 获取部门列表 */
  897. function getDepartmentList() {
  898. listDept({
  899. pageNum: 1,
  900. pageSize: 1000, // 获取所有部门
  901. status: 0 // 修复:改为number类型
  902. })
  903. .then((response) => {
  904. const deptList = response.data || [];
  905. // 过滤出二级学院和行政单位下的部门,作为招聘部门选项
  906. const filteredDepts = deptList.filter((dept: DeptVO) => {
  907. // 包含二级学院(101)和行政单位(102)下的部门,排除顶级部门
  908. return dept.parentId === 101 || dept.parentId === 102;
  909. });
  910. departmentOptions.value = filteredDepts.map((dept: DeptVO) => ({
  911. value: dept.deptName,
  912. label: dept.deptName
  913. }));
  914. console.log('部门选项已更新:', departmentOptions.value);
  915. })
  916. .catch((error) => {
  917. console.error('获取部门列表失败:', error);
  918. // 使用备用部门数据
  919. departmentOptions.value = [
  920. { value: '人文学院', label: '人文学院' },
  921. { value: '智能控制学院', label: '智能控制学院' },
  922. { value: '智能制造学院', label: '智能制造学院' },
  923. { value: '医药健康学院', label: '医药健康学院' },
  924. { value: '餐饮休闲学院', label: '餐饮休闲学院' },
  925. { value: '数字信息学院', label: '数字信息学院' },
  926. { value: '生态园林学院', label: '生态园林学院' },
  927. { value: '人事处', label: '人事处' },
  928. { value: '教务处', label: '教务处' },
  929. { value: '学生处', label: '学生处' },
  930. { value: '总务处', label: '总务处' }
  931. ];
  932. });
  933. }
  934. /** 模拟职位数据 */
  935. function mockJobData() {
  936. // 创建基础模拟数据
  937. const mockData = [
  938. {
  939. jobId: 1,
  940. title: 'Java开发工程师',
  941. department: 'tech',
  942. salaryRange: '15k-25k',
  943. location: '杭州',
  944. description: '负责校园教学管理系统的开发和维护...',
  945. requirements: '计算机相关专业本科及以上学历,3年以上Java开发经验...',
  946. isActive: '0',
  947. viewCount: 356,
  948. applicantCount: 12,
  949. publishTime: '2025-06-01 10:00:00',
  950. createTime: '2025-06-01 10:00:00',
  951. updateTime: '2025-06-15 15:30:00',
  952. createBy: '1',
  953. updateBy: '1',
  954. remark: ''
  955. },
  956. {
  957. jobId: 2,
  958. title: '前端工程师',
  959. department: 'tech',
  960. salaryRange: '12k-18k',
  961. location: '杭州',
  962. description: '负责校园教学管理系统前端开发...',
  963. requirements: '计算机相关专业本科及以上学历,2年以上前端开发经验...',
  964. isActive: '0',
  965. viewCount: 245,
  966. applicantCount: 8,
  967. publishTime: '2025-06-05 14:30:00',
  968. createTime: '2025-06-05 14:30:00',
  969. updateTime: '2025-06-15 15:30:00',
  970. createBy: '1',
  971. updateBy: '1',
  972. remark: ''
  973. },
  974. {
  975. jobId: 3,
  976. title: '行政助理',
  977. department: 'admin',
  978. salaryRange: '6k-8k',
  979. location: '杭州',
  980. description: '负责日常行政工作...',
  981. requirements: '行政管理相关专业,1年以上相关工作经验...',
  982. isActive: '0',
  983. viewCount: 178,
  984. applicantCount: 15,
  985. publishTime: '2025-06-10 09:15:00',
  986. createTime: '2025-06-10 09:15:00',
  987. updateTime: '2025-06-15 15:30:00',
  988. createBy: '1',
  989. updateBy: '1',
  990. remark: ''
  991. }
  992. ];
  993. // 应用筛选
  994. let filteredData = [...mockData];
  995. // 按职位名称筛选
  996. if (queryParams.title) {
  997. filteredData = filteredData.filter((item) => item.title.includes(queryParams.title));
  998. }
  999. // 按部门筛选
  1000. if (queryParams.department) {
  1001. filteredData = filteredData.filter((item) => item.department === queryParams.department);
  1002. }
  1003. // 按状态筛选
  1004. if (queryParams.isActive) {
  1005. filteredData = filteredData.filter((item) => item.isActive === queryParams.isActive);
  1006. }
  1007. // 设置数据
  1008. jobList.value = filteredData;
  1009. total.value = filteredData.length;
  1010. }
  1011. /** 取消按钮 */
  1012. function cancel() {
  1013. open.value = false;
  1014. reset();
  1015. }
  1016. /** 表单重置 */
  1017. function reset() {
  1018. form.jobId = undefined;
  1019. form.title = '';
  1020. form.department = '';
  1021. form.salaryRange = '';
  1022. form.location = '';
  1023. form.description = '';
  1024. form.requirements = '';
  1025. form.isActive = '0';
  1026. form.publishTime = '';
  1027. form.remark = '';
  1028. jobForm.value?.resetFields();
  1029. }
  1030. /** 搜索按钮操作 */
  1031. function handleQuery() {
  1032. queryParams.pageNum = 1;
  1033. getList();
  1034. }
  1035. /** 重置按钮操作 */
  1036. function resetQuery() {
  1037. queryForm.value?.resetFields();
  1038. handleQuery();
  1039. }
  1040. /** 多选框选中数据 */
  1041. function handleSelectionChange(selection: JobVO[]) {
  1042. ids.value = selection.map((item) => item.jobId);
  1043. single.value = selection.length !== 1;
  1044. multiple.value = !selection.length;
  1045. }
  1046. /** 新增按钮操作 */
  1047. function handleAdd() {
  1048. reset();
  1049. open.value = true;
  1050. title.value = '添加职位';
  1051. }
  1052. /** 修改按钮操作 */
  1053. function handleUpdate(row?: JobVO) {
  1054. reset();
  1055. const jobId = row?.jobId || ids.value[0];
  1056. getJob(jobId).then((response) => {
  1057. Object.assign(form, response.data);
  1058. open.value = true;
  1059. title.value = '修改职位';
  1060. });
  1061. }
  1062. /** 查看按钮操作 */
  1063. function handleView(row: JobVO) {
  1064. getJob(row.jobId)
  1065. .then((response) => {
  1066. Object.assign(viewForm, response.data);
  1067. viewOpen.value = true;
  1068. })
  1069. .catch((error) => {
  1070. console.error('获取职位详情失败:', error);
  1071. // 如果API调用失败,使用行数据
  1072. Object.assign(viewForm, row);
  1073. viewOpen.value = true;
  1074. });
  1075. }
  1076. /** 提交按钮 */
  1077. function submitForm() {
  1078. jobForm.value?.validate((valid: boolean) => {
  1079. if (valid) {
  1080. buttonLoading.value = true;
  1081. if (form.jobId) {
  1082. updateJob(form)
  1083. .then(() => {
  1084. proxy?.$modal.msgSuccess('修改成功');
  1085. open.value = false;
  1086. getList();
  1087. })
  1088. .finally(() => {
  1089. buttonLoading.value = false;
  1090. });
  1091. } else {
  1092. addJob(form)
  1093. .then(() => {
  1094. proxy?.$modal.msgSuccess('新增成功');
  1095. open.value = false;
  1096. getList();
  1097. })
  1098. .finally(() => {
  1099. buttonLoading.value = false;
  1100. });
  1101. }
  1102. }
  1103. });
  1104. }
  1105. /** 删除按钮操作 */
  1106. function handleDelete(row?: JobVO) {
  1107. const jobIds = row?.jobId || ids.value;
  1108. let confirmMessage = '';
  1109. if (row) {
  1110. // 单个删除
  1111. confirmMessage = `是否确认删除职位"${row.title}"?`;
  1112. } else {
  1113. // 批量删除
  1114. const selectedItems = jobList.value.filter((item) => ids.value.includes(item.jobId));
  1115. if (selectedItems.length === 1) {
  1116. confirmMessage = `是否确认删除职位"${selectedItems[0].title}"?`;
  1117. } else {
  1118. const names = selectedItems.map((item) => item.title).join('、');
  1119. confirmMessage = `是否确认删除职位"${names}"?`;
  1120. }
  1121. }
  1122. proxy?.$modal
  1123. .confirm(confirmMessage)
  1124. .then(() => {
  1125. return delJob(jobIds);
  1126. })
  1127. .then(() => {
  1128. getList();
  1129. proxy?.$modal.msgSuccess('删除成功');
  1130. });
  1131. }
  1132. /** 导出按钮操作 */
  1133. function handleExport() {
  1134. proxy?.download(
  1135. 'rs/job/export',
  1136. {
  1137. ...queryParams
  1138. },
  1139. `job_${new Date().getTime()}.xlsx`
  1140. );
  1141. }
  1142. /** 切换职位状态 */
  1143. function handleToggleStatus(row: JobVO) {
  1144. const text = row.isActive === '0' ? '停止招聘' : '开始招聘';
  1145. proxy?.$modal
  1146. .confirm(`确认要${text}该职位吗?`)
  1147. .then(() => {
  1148. return toggleJobStatus(row.jobId);
  1149. })
  1150. .then(() => {
  1151. getList();
  1152. proxy?.$modal.msgSuccess(`${text}成功`);
  1153. });
  1154. }
  1155. /** 查询申请记录列表 */
  1156. function getApplicationList() {
  1157. applicationLoading.value = true;
  1158. // 如果是从职位详情过来,使用当前职位ID
  1159. if (currentJob.value?.jobId) {
  1160. listApplications(currentJob.value.jobId, applicationQueryParams)
  1161. .then((response) => {
  1162. applicationList.value = response.rows || [];
  1163. applicationTotal.value = response.total || 0;
  1164. applicationLoading.value = false;
  1165. })
  1166. .catch(() => {
  1167. // 加载失败时使用模拟数据
  1168. mockApplicationData();
  1169. });
  1170. }
  1171. // 如果是从申请管理选项卡过来,查询所有申请记录
  1172. else {
  1173. listAllApplications(applicationQueryParams)
  1174. .then((response) => {
  1175. applicationList.value = response.rows || [];
  1176. applicationTotal.value = response.total || 0;
  1177. applicationLoading.value = false;
  1178. })
  1179. .catch(() => {
  1180. // 加载失败时使用模拟数据
  1181. mockApplicationData();
  1182. });
  1183. }
  1184. }
  1185. /** 模拟申请数据 */
  1186. function mockApplicationData() {
  1187. const mockData: ApplicationVO[] = [
  1188. {
  1189. applicationId: 1,
  1190. jobId: 1,
  1191. jobTitle: 'Java开发工程师',
  1192. applicantId: 1,
  1193. applicantName: '张文明',
  1194. gender: '男', // 新增:性别
  1195. age: 25, // 新增:年龄
  1196. status: '0',
  1197. applyTime: '2025-07-25 10:30:00',
  1198. resumePath: '/uploads/resume/zhangwm_resume.pdf',
  1199. selfDescription: '本科计算机科学与技术专业毕业,熟悉Java开发技术栈,有2年相关工作经验,对Spring Boot、MyBatis等框架有深入了解。',
  1200. hrNotes: '初步筛选通过,安排下周一进行技术面试',
  1201. contactPhone: '13812345678',
  1202. contactEmail: 'zhangwm@example.com',
  1203. education: '2018-2022 浙江大学 计算机科学与技术 本科',
  1204. educationBackground: '2018-2022 浙江大学 计算机科学与技术 本科', // 新增:教育背景
  1205. experience: '2022-2025 杭州某科技公司 Java开发工程师',
  1206. remark: '候选人技术基础扎实,沟通能力良好',
  1207. createBy: 'admin',
  1208. createTime: '2025-07-25 10:30:00',
  1209. updateBy: 'hr001',
  1210. updateTime: '2025-07-25 14:20:00'
  1211. },
  1212. {
  1213. applicationId: 2,
  1214. jobId: 1,
  1215. jobTitle: 'Java开发工程师',
  1216. applicantId: 2,
  1217. applicantName: '李小红',
  1218. gender: '女', // 新增:性别
  1219. age: 23, // 新增:年龄
  1220. status: '1',
  1221. applyTime: '2025-07-24 16:45:00',
  1222. resumePath: '/uploads/resume/lixh_resume.pdf',
  1223. selfDescription: '计算机相关专业应届毕业生,在校期间参与过多个项目开发,熟悉Java、MySQL、Redis等技术。',
  1224. hrNotes: '应届生,技术基础待考察',
  1225. contactPhone: '13987654321',
  1226. contactEmail: 'lixh@example.com',
  1227. education: '2020-2024 杭州电子科技大学 软件工程 本科',
  1228. educationBackground: '2020-2024 杭州电子科技大学 软件工程 本科', // 新增:教育背景
  1229. experience: '在校期间参与学校实验室项目开发',
  1230. remark: '应届毕业生,可培养',
  1231. createBy: 'admin',
  1232. createTime: '2025-07-24 16:45:00',
  1233. updateBy: 'hr001',
  1234. updateTime: '2025-07-24 17:30:00'
  1235. },
  1236. {
  1237. applicationId: 3,
  1238. jobId: 2,
  1239. jobTitle: 'UI设计师',
  1240. applicantId: 3,
  1241. applicantName: '王设计',
  1242. gender: '女', // 新增:性别
  1243. age: 27, // 新增:年龄
  1244. status: '2',
  1245. applyTime: '2025-07-23 14:20:00',
  1246. resumePath: '/uploads/resume/wangdesign_resume.pdf',
  1247. selfDescription: '5年UI/UX设计经验,擅长移动端和Web端界面设计,熟练使用Figma、Sketch、Adobe系列软件。',
  1248. hrNotes: '作品集质量很高,已通过初试',
  1249. contactPhone: '13612345678',
  1250. contactEmail: 'wangdesign@example.com',
  1251. education: '2015-2019 中国美术学院 视觉传达设计 本科',
  1252. educationBackground: '2015-2019 中国美术学院 视觉传达设计 本科', // 新增:教育背景
  1253. experience: '2019-2024 某互联网公司 UI设计师',
  1254. remark: '设计能力强,有良好的审美',
  1255. createBy: 'admin',
  1256. createTime: '2025-07-23 14:20:00',
  1257. updateBy: 'hr002',
  1258. updateTime: '2025-07-23 16:45:00'
  1259. },
  1260. {
  1261. applicationId: 4,
  1262. jobId: 3,
  1263. jobTitle: '产品经理',
  1264. applicantId: 4,
  1265. applicantName: '陈产品',
  1266. gender: '男', // 新增:性别
  1267. age: 30, // 新增:年龄
  1268. status: '3',
  1269. applyTime: '2025-07-22 11:15:00',
  1270. resumePath: '/uploads/resume/chenchanpin_resume.pdf',
  1271. selfDescription: '8年产品经验,负责过多个从0到1的产品项目,具备完整的产品生命周期管理能力。',
  1272. hrNotes: '面试表现优秀,待薪资谈判',
  1273. contactPhone: '13512345678',
  1274. contactEmail: 'chenchanpin@example.com',
  1275. education: '2012-2016 浙江大学 工商管理 本科',
  1276. educationBackground: '2012-2016 浙江大学 工商管理 本科', // 新增:教育背景
  1277. experience: '2016-2024 多家互联网公司 产品经理',
  1278. remark: '产品思维清晰,有丰富的实战经验',
  1279. createBy: 'admin',
  1280. createTime: '2025-07-22 11:15:00',
  1281. updateBy: 'hr003',
  1282. updateTime: '2025-07-22 15:30:00'
  1283. },
  1284. {
  1285. applicationId: 5,
  1286. jobId: 4,
  1287. jobTitle: '市场专员',
  1288. applicantId: 5,
  1289. applicantName: '刘市场',
  1290. gender: '女', // 新增:性别
  1291. age: 24, // 新增:年龄
  1292. status: '4',
  1293. applyTime: '2025-07-21 09:30:00',
  1294. resumePath: '/uploads/resume/liushichang_resume.pdf',
  1295. selfDescription: '市场营销专业毕业,有2年市场推广经验,熟悉新媒体运营和活动策划。',
  1296. hrNotes: '不符合岗位要求,建议拒绝',
  1297. contactPhone: '13412345678',
  1298. contactEmail: 'liushichang@example.com',
  1299. education: '2018-2022 浙江工商大学 市场营销 本科',
  1300. educationBackground: '2018-2022 浙江工商大学 市场营销 本科', // 新增:教育背景
  1301. experience: '2022-2024 某传媒公司 市场专员',
  1302. remark: '经验不足,与岗位需求不匹配',
  1303. createBy: 'admin',
  1304. createTime: '2025-07-21 09:30:00',
  1305. updateBy: 'hr001',
  1306. updateTime: '2025-07-21 11:45:00'
  1307. }
  1308. ];
  1309. // 应用搜索过滤
  1310. let filteredData = mockData;
  1311. if (applicationQueryParams.applicantName) {
  1312. filteredData = filteredData.filter((item) => item.applicantName.includes(applicationQueryParams.applicantName));
  1313. }
  1314. if (applicationQueryParams.contactPhone) {
  1315. filteredData = filteredData.filter((item) => item.contactPhone.includes(applicationQueryParams.contactPhone));
  1316. }
  1317. if (applicationQueryParams.status) {
  1318. filteredData = filteredData.filter((item) => item.status === applicationQueryParams.status);
  1319. }
  1320. // 分页处理
  1321. const startIndex = (applicationQueryParams.pageNum - 1) * applicationQueryParams.pageSize;
  1322. const endIndex = startIndex + applicationQueryParams.pageSize;
  1323. const pageData = filteredData.slice(startIndex, endIndex);
  1324. applicationList.value = pageData;
  1325. applicationTotal.value = filteredData.length;
  1326. applicationLoading.value = false;
  1327. }
  1328. /** 查看申请记录 */
  1329. function handleApplications(row: JobVO) {
  1330. currentJob.value = row;
  1331. applicationQueryParams.pageNum = 1;
  1332. applicationOpen.value = true;
  1333. getApplicationList();
  1334. }
  1335. /** 查看应聘者详情 */
  1336. function handleViewApplicant(row: ApplicationVO) {
  1337. currentApplication.value = row;
  1338. applicantViewOpen.value = true;
  1339. }
  1340. /** 编辑应聘者信息 */
  1341. function handleEditApplicant(row: ApplicationVO) {
  1342. // 复制数据到编辑表单
  1343. Object.assign(editForm, row);
  1344. applicantEditOpen.value = true;
  1345. }
  1346. /** 提交编辑 */
  1347. function submitEdit() {
  1348. console.log('提交编辑开始', {
  1349. applicationId: editForm.applicationId,
  1350. status: editForm.status,
  1351. hrNotes: editForm.hrNotes
  1352. });
  1353. if (!editForm.applicationId) {
  1354. proxy?.$modal.msgError('应聘记录ID不能为空');
  1355. return;
  1356. }
  1357. if (!editForm.status) {
  1358. proxy?.$modal.msgError('请选择申请状态');
  1359. return;
  1360. }
  1361. buttonLoading.value = true;
  1362. // 调用更新API,包含状态和HR备注
  1363. updateApplicationInfo(editForm.applicationId, {
  1364. status: editForm.status,
  1365. hrNotes: editForm.hrNotes || ''
  1366. })
  1367. .then((response) => {
  1368. console.log('API调用成功', response);
  1369. proxy?.$modal.msgSuccess('修改成功');
  1370. applicantEditOpen.value = false;
  1371. // 更新列表中的数据
  1372. const index = applicationList.value.findIndex((item) => item.applicationId === editForm.applicationId);
  1373. if (index !== -1) {
  1374. applicationList.value[index].status = editForm.status;
  1375. applicationList.value[index].hrNotes = editForm.hrNotes;
  1376. }
  1377. buttonLoading.value = false;
  1378. })
  1379. .catch((error) => {
  1380. console.error('API调用失败', error);
  1381. proxy?.$modal.msgError('修改失败: ' + (error.message || '未知错误'));
  1382. buttonLoading.value = false;
  1383. });
  1384. }
  1385. /** 更新申请状态 */
  1386. function handleUpdateStatus(row: ApplicationVO, status: string) {
  1387. const statusText = status === '1' ? '通过' : '拒绝';
  1388. proxy?.$modal
  1389. .confirm(`确认要${statusText}该申请吗?`)
  1390. .then(() => {
  1391. return updateApplicationStatus(row.applicationId, status);
  1392. })
  1393. .then(() => {
  1394. getApplicationList();
  1395. proxy?.$modal.msgSuccess(`${statusText}成功`);
  1396. });
  1397. }
  1398. /** 处理状态变更 */
  1399. function handleStatusChange(row: ApplicationVO, newStatus: string) {
  1400. if (newStatus === row.status) {
  1401. return; // 状态未变更,无需处理
  1402. }
  1403. const statusOption = applicationStatusOptions.find((option) => option.value === newStatus);
  1404. const statusText = statusOption ? statusOption.label : newStatus;
  1405. proxy?.$modal
  1406. .confirm(`确认要将申请状态修改为"${statusText}"吗?`)
  1407. .then(() => {
  1408. return updateApplicationStatus(row.applicationId, newStatus);
  1409. })
  1410. .then(() => {
  1411. // 更新本地数据
  1412. row.status = newStatus;
  1413. proxy?.$modal.msgSuccess(`状态修改成功`);
  1414. })
  1415. .catch(() => {
  1416. // 如果操作被取消或失败,刷新列表以恢复原状态
  1417. getApplicationList();
  1418. });
  1419. }
  1420. /** 获取部门标签 */
  1421. function getDepartmentLabel(value: string): string {
  1422. const option = departmentOptions.value.find((item) => item.value === value);
  1423. return option ? option.label : value;
  1424. }
  1425. /** 获取申请状态标签 */
  1426. function getStatusLabel(value: string): string {
  1427. const option = applicationStatusOptions.find((item) => item.value === value);
  1428. return option ? option.label : value;
  1429. }
  1430. /** 获取申请状态类型 */
  1431. function getStatusType(value: string): 'primary' | 'success' | 'info' | 'warning' | 'danger' {
  1432. const option = applicationStatusOptions.find((item) => item.value === value);
  1433. return (option?.type as 'primary' | 'success' | 'info' | 'warning' | 'danger') || 'info';
  1434. }
  1435. /** 发送邮件 */
  1436. function sendEmail(email: string, applicantName: string) {
  1437. if (!email) {
  1438. ElMessage.warning('邮箱地址为空');
  1439. return;
  1440. }
  1441. const subject = encodeURIComponent(`关于您的求职申请 - ${applicantName}`);
  1442. const body = encodeURIComponent(`亲爱的 ${applicantName}:\n\n感谢您对我们的关注和求职申请。\n\n我们将仔细审核您的申请材料,如有需要会及时与您联系。\n\n祝您工作顺利!\n\n此致\n敬礼\n\nHR部门`);
  1443. const mailtoUrl = `mailto:${email}?subject=${subject}&body=${body}`;
  1444. try {
  1445. window.open(mailtoUrl);
  1446. ElMessage.success('已打开邮件客户端');
  1447. } catch (error) {
  1448. ElMessage.error('打开邮件客户端失败,请检查系统设置');
  1449. }
  1450. }
  1451. /** 选项卡切换 */
  1452. function handleTabClick() {
  1453. if (activeTab.value === 'application') {
  1454. getApplicationList();
  1455. } else if (activeTab.value === 'recruitment') {
  1456. getList();
  1457. }
  1458. }
  1459. /** 下载简历 */
  1460. function downloadResume(resumePath: string) {
  1461. if (!resumePath) {
  1462. proxy?.$modal.msgError('简历文件不存在');
  1463. return;
  1464. }
  1465. // 实际下载实现可能需要调用后端API
  1466. ElMessage.success('简历下载功能开发中');
  1467. }
  1468. /** 导出申请记录 */
  1469. function handleExportApplications() {
  1470. proxy?.download(
  1471. 'rs/application/export',
  1472. {
  1473. ...applicationQueryParams
  1474. },
  1475. `application_${new Date().getTime()}.xlsx`
  1476. );
  1477. }
  1478. /** 打印简历记录 */
  1479. function handlePrintApplications() {
  1480. // 创建打印区域
  1481. const printContainer = document.createElement('iframe');
  1482. printContainer.style.position = 'fixed';
  1483. printContainer.style.right = '-1000px';
  1484. printContainer.style.top = '0';
  1485. printContainer.style.width = '800px';
  1486. printContainer.style.height = '100%';
  1487. printContainer.style.border = 'none';
  1488. document.body.appendChild(printContainer);
  1489. // 创建打印内容
  1490. const printDocument = printContainer.contentWindow.document;
  1491. // 写入基本HTML结构
  1492. printDocument.open();
  1493. printDocument.write('<!DOCTYPE html><html><head><title>简历记录打印</title>');
  1494. // 添加样式
  1495. printDocument.write('<style>');
  1496. printDocument.write('body { font-family: Arial, sans-serif; margin: 20px; }');
  1497. printDocument.write('table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }');
  1498. printDocument.write('table, th, td { border: 1px solid #dcdfe6; }');
  1499. printDocument.write('th, td { padding: 8px; text-align: center; }');
  1500. printDocument.write('th { background-color: #f5f7fa; }');
  1501. printDocument.write('.title { text-align: center; font-size: 20px; font-weight: bold; margin-bottom: 20px; }');
  1502. printDocument.write('.info { margin-bottom: 15px; }');
  1503. printDocument.write('.tag { padding: 2px 6px; border-radius: 4px; display: inline-block; }');
  1504. printDocument.write('.tag-pending { background-color: #e6a23c; color: white; }');
  1505. printDocument.write('.tag-success { background-color: #67c23a; color: white; }');
  1506. printDocument.write('.tag-danger { background-color: #f56c6c; color: white; }');
  1507. printDocument.write('</style></head><body>');
  1508. // 添加标题和信息
  1509. printDocument.write('<div class="title">简历管理列表</div>');
  1510. printDocument.write('<div class="info">');
  1511. printDocument.write('<p>打印时间: ' + parseTime(new Date()) + '</p>');
  1512. if (currentJob.value) {
  1513. printDocument.write('<p>职位: ' + currentJob.value.title + '</p>');
  1514. }
  1515. printDocument.write('<p>总记录数: ' + applicationTotal.value + '</p>');
  1516. printDocument.write('</div>');
  1517. // 添加表格
  1518. printDocument.write('<table>');
  1519. printDocument.write('<thead><tr>');
  1520. printDocument.write('<th>职位名称</th>');
  1521. printDocument.write('<th>应聘者</th>');
  1522. printDocument.write('<th>申请状态</th>');
  1523. printDocument.write('<th>联系电话</th>');
  1524. printDocument.write('<th>联系邮箱</th>');
  1525. printDocument.write('<th>申请时间</th>');
  1526. printDocument.write('</tr></thead>');
  1527. printDocument.write('<tbody>');
  1528. // 添加数据行
  1529. applicationList.value.forEach((item) => {
  1530. let statusClass = '';
  1531. if (item.status === '0') statusClass = 'tag-pending';
  1532. else if (item.status === '1') statusClass = 'tag-success';
  1533. else if (item.status === '2') statusClass = 'tag-danger';
  1534. printDocument.write('<tr>');
  1535. printDocument.write('<td>' + (item.jobTitle || '') + '</td>');
  1536. printDocument.write('<td>' + (item.applicantName || '') + '</td>');
  1537. printDocument.write('<td><span class="tag ' + statusClass + '">' + getStatusLabel(item.status) + '</span></td>');
  1538. printDocument.write('<td>' + (item.contactPhone || '') + '</td>');
  1539. printDocument.write('<td>' + (item.contactEmail || '') + '</td>');
  1540. printDocument.write('<td>' + (parseTime(item.applyTime) || '') + '</td>');
  1541. printDocument.write('</tr>');
  1542. });
  1543. printDocument.write('</tbody></table>');
  1544. printDocument.write('</body></html>');
  1545. printDocument.close();
  1546. // 等待内容加载完成后打印
  1547. printContainer.onload = () => {
  1548. try {
  1549. printContainer.contentWindow.print();
  1550. // 打印完成后移除iframe
  1551. setTimeout(() => {
  1552. document.body.removeChild(printContainer);
  1553. }, 1000);
  1554. } catch (e) {
  1555. console.error('打印出错:', e);
  1556. document.body.removeChild(printContainer);
  1557. proxy?.$modal.msgError('打印过程中出现错误');
  1558. }
  1559. };
  1560. }
  1561. /** 保存HR备注 */
  1562. function saveHrNotes() {
  1563. updateApplicationStatus(applicationDetail.applicationId, applicationDetail.status).then(() => {
  1564. proxy?.$modal.msgSuccess('保存成功');
  1565. applicationDetailOpen.value = false;
  1566. getApplicationList();
  1567. });
  1568. }
  1569. /** 搜索按钮操作 - 申请管理 */
  1570. function handleApplicationQuery() {
  1571. applicationQueryParams.pageNum = 1;
  1572. getApplicationList();
  1573. }
  1574. /** 重置按钮操作 - 申请管理 */
  1575. function resetApplicationQuery() {
  1576. const applicationQueryFormRef = proxy?.$refs.applicationQueryForm as any;
  1577. if (applicationQueryFormRef) {
  1578. applicationQueryFormRef.resetFields();
  1579. }
  1580. applicationQueryParams.jobId = undefined;
  1581. applicationQueryParams.jobTitle = '';
  1582. applicationQueryParams.applicantName = '';
  1583. applicationQueryParams.contactPhone = '';
  1584. applicationQueryParams.status = '';
  1585. handleApplicationQuery();
  1586. }
  1587. /** 多选框选中数据 - 申请管理 */
  1588. function handleApplicationSelectionChange(selection: ApplicationVO[]) {
  1589. applicationIds.value = selection.map((item) => item.applicationId);
  1590. applicationSingle.value = selection.length !== 1;
  1591. applicationMultiple.value = !selection.length;
  1592. }
  1593. /** 上传附件文件 */
  1594. function uploadAttachmentFile(params: any, applicationId: string | number) {
  1595. const file = params.file;
  1596. uploadAttachment(applicationId, file)
  1597. .then((response) => {
  1598. if (response.code === 200) {
  1599. ElMessage.success('附件上传成功');
  1600. // 重新获取申请详情
  1601. getApplication(applicationId as number).then((res) => {
  1602. if (res.code === 200) {
  1603. applicationDetail.attachmentPath = res.data.attachmentPath;
  1604. applicationDetail.attachmentName = res.data.attachmentName;
  1605. }
  1606. });
  1607. getApplicationList(); // 刷新申请列表
  1608. } else {
  1609. ElMessage.error(response.msg || '附件上传失败');
  1610. }
  1611. })
  1612. .catch((error) => {
  1613. console.error('上传附件失败:', error);
  1614. ElMessage.error('附件上传失败');
  1615. });
  1616. }
  1617. /** 预览附件 */
  1618. function previewAttachment(row: ApplicationVO) {
  1619. if (row.attachmentPath) {
  1620. // 使用浏览器原生方法打开附件
  1621. window.open(import.meta.env.VITE_APP_BASE_API + '/' + row.attachmentPath, '_blank');
  1622. } else {
  1623. ElMessage.warning('附件不存在');
  1624. }
  1625. }
  1626. /** 导出简历为PDF */
  1627. function handleExportResume() {
  1628. if (applicationIds.value.length !== 1) {
  1629. proxy?.$modal.msgWarning('请选择一条简历记录');
  1630. return;
  1631. }
  1632. // 获取选中的申请记录
  1633. const selectedId = applicationIds.value[0];
  1634. const selectedRecord = applicationList.value.find((item) => item.applicationId === selectedId);
  1635. if (!selectedRecord) {
  1636. proxy?.$modal.msgError('未找到选中的记录');
  1637. return;
  1638. }
  1639. // 显示加载提示
  1640. proxy?.$modal.loading('正在生成PDF文件,请稍候...');
  1641. try {
  1642. // 创建一个临时的HTML元素来渲染简历内容
  1643. const resumeContainer = document.createElement('div');
  1644. resumeContainer.style.width = '800px';
  1645. resumeContainer.style.padding = '40px';
  1646. resumeContainer.style.position = 'absolute';
  1647. resumeContainer.style.left = '-9999px';
  1648. resumeContainer.style.fontFamily = 'Arial, "Microsoft YaHei", sans-serif'; // 使用系统字体
  1649. // 添加简历内容
  1650. resumeContainer.innerHTML = `
  1651. <div style="text-align: center; margin-bottom: 30px;">
  1652. <h1 style="font-size: 24px; margin-bottom: 10px;">简历详情</h1>
  1653. <div style="border-bottom: 1px solid #ddd; margin-bottom: 20px;"></div>
  1654. </div>
  1655. <div style="margin-bottom: 30px;">
  1656. <h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">基本信息</h2>
  1657. <p><strong>姓名:</strong>${selectedRecord.applicantName || '未提供'}</p>
  1658. <p><strong>应聘职位:</strong>${selectedRecord.jobTitle || '未提供'}</p>
  1659. <p><strong>联系电话:</strong>${selectedRecord.contactPhone || '未提供'}</p>
  1660. <p><strong>联系邮箱:</strong>${selectedRecord.contactEmail || '未提供'}</p>
  1661. </div>
  1662. <div style="margin-bottom: 30px;">
  1663. <h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">教育背景</h2>
  1664. <p><strong>最高学历:</strong>${selectedRecord.education || '未提供'}</p>
  1665. </div>
  1666. <div style="margin-bottom: 30px;">
  1667. <h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">工作经验</h2>
  1668. <p style="white-space: pre-wrap;">${selectedRecord.experience || '未提供工作经验'}</p>
  1669. </div>
  1670. <div style="margin-bottom: 30px;">
  1671. <h2 style="font-size: 18px; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 5px;">自我介绍</h2>
  1672. <p style="white-space: pre-wrap;">${selectedRecord.selfDescription || '未提供自我介绍'}</p>
  1673. </div>
  1674. <div style="margin-top: 50px; text-align: right; font-size: 12px; color: #999;">
  1675. 生成日期:${parseTime(new Date())}
  1676. </div>
  1677. `;
  1678. // 将元素添加到文档中
  1679. document.body.appendChild(resumeContainer);
  1680. // 使用html2canvas将HTML转换为canvas
  1681. html2canvas(resumeContainer, {
  1682. scale: 2, // 提高清晰度
  1683. useCORS: true,
  1684. logging: false
  1685. })
  1686. .then((canvas) => {
  1687. // 从canvas创建PDF
  1688. const imgData = canvas.toDataURL('image/png');
  1689. const pdf = new jsPDF({
  1690. orientation: 'portrait',
  1691. unit: 'mm',
  1692. format: 'a4'
  1693. });
  1694. // 计算PDF页面尺寸和图像比例
  1695. const imgWidth = 210; // A4宽度,单位是mm
  1696. const pageHeight = 297; // A4高度,单位是mm
  1697. const imgHeight = (canvas.height * imgWidth) / canvas.width;
  1698. let heightLeft = imgHeight;
  1699. let position = 0;
  1700. // 添加第一页
  1701. pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
  1702. heightLeft -= pageHeight;
  1703. // 如果内容超过一页,添加新页面
  1704. while (heightLeft > 0) {
  1705. position = heightLeft - imgHeight;
  1706. pdf.addPage();
  1707. pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
  1708. heightLeft -= pageHeight;
  1709. }
  1710. // 保存PDF
  1711. pdf.save(`简历_${selectedRecord.applicantName}_${new Date().getTime()}.pdf`);
  1712. // 移除临时元素
  1713. document.body.removeChild(resumeContainer);
  1714. proxy?.$modal.closeLoading();
  1715. proxy?.$modal.msgSuccess('PDF导出成功');
  1716. })
  1717. .catch((error) => {
  1718. console.error('PDF生成失败:', error);
  1719. document.body.removeChild(resumeContainer);
  1720. proxy?.$modal.closeLoading();
  1721. proxy?.$modal.msgError('PDF导出失败,请重试');
  1722. });
  1723. } catch (error) {
  1724. console.error('PDF导出失败:', error);
  1725. proxy?.$modal.closeLoading();
  1726. proxy?.$modal.msgError('PDF导出失败,请重试');
  1727. }
  1728. }
  1729. onMounted(() => {
  1730. // 加载招聘职位数据
  1731. getList();
  1732. // 加载简历管理数据,确保第一次进入时简历管理也有数据
  1733. getApplicationList();
  1734. // 加载部门数据,确保部门选项来自数据库
  1735. getDepartmentList();
  1736. });
  1737. </script>
  1738. <style scoped>
  1739. .card-header {
  1740. display: flex;
  1741. justify-content: space-between;
  1742. align-items: center;
  1743. }
  1744. .tab-content {
  1745. padding: 20px 0;
  1746. }
  1747. .el-tag {
  1748. margin-right: 5px;
  1749. }
  1750. /* 页面容器样式 */
  1751. .app-container {
  1752. padding: 8px;
  1753. }
  1754. /* 表格卡片布局 */
  1755. .table-card {
  1756. margin-top: 0;
  1757. }
  1758. .table-card :deep(.el-card__body) {
  1759. padding: 0px 20px 20px 20px !important;
  1760. }
  1761. /* 移除按钮栏底部边框 */
  1762. .button-card :deep(.el-card__header) {
  1763. border-bottom: none !important;
  1764. }
  1765. .view-description {
  1766. padding: 12px;
  1767. border: 1px solid #e4e7ed;
  1768. border-radius: 4px;
  1769. width: 100%;
  1770. height: 150px;
  1771. font-size: 14px;
  1772. color: #303133;
  1773. line-height: 1.6;
  1774. white-space: pre-wrap;
  1775. word-break: break-word;
  1776. overflow-y: auto;
  1777. }
  1778. /* 分页容器样式 */
  1779. .pagination-container {
  1780. display: flex;
  1781. justify-content: flex-end;
  1782. padding: 8px 20px 0 0 !important;
  1783. background-color: #fff;
  1784. margin-top: 0px !important;
  1785. }
  1786. .pagination-container :deep(.el-pagination) {
  1787. font-size: 12px;
  1788. }
  1789. .pagination-container :deep(.el-pagination .btn-prev),
  1790. .pagination-container :deep(.el-pagination .btn-next),
  1791. .pagination-container :deep(.el-pagination .el-pager li) {
  1792. min-width: 28px;
  1793. height: 28px;
  1794. line-height: 28px;
  1795. font-size: 12px;
  1796. }
  1797. /* 搜索按钮容器样式 */
  1798. .search-btn-container {
  1799. display: flex;
  1800. justify-content: flex-end;
  1801. margin-top: 10px;
  1802. }
  1803. .search-btn-container :deep(.el-form-item) {
  1804. margin-bottom: 0 !important;
  1805. }
  1806. /* 申请记录样式 */
  1807. .job-info-header {
  1808. margin-bottom: 8px;
  1809. }
  1810. /* 紧凑型职位信息样式 */
  1811. .job-info-compact {
  1812. display: flex;
  1813. align-items: center;
  1814. padding: 6px 10px;
  1815. background: #f8f9fa;
  1816. border: 1px solid #e1e8ed;
  1817. border-radius: 4px;
  1818. gap: 8px;
  1819. flex-wrap: wrap;
  1820. min-height: 36px;
  1821. }
  1822. .info-item {
  1823. display: flex;
  1824. align-items: center;
  1825. gap: 3px;
  1826. height: 24px;
  1827. }
  1828. .info-label {
  1829. font-size: 13px;
  1830. color: #606266;
  1831. font-weight: 500;
  1832. line-height: 1.3;
  1833. }
  1834. .info-value {
  1835. font-size: 13px;
  1836. color: #303133;
  1837. font-weight: 600;
  1838. line-height: 1.3;
  1839. }
  1840. .info-value.highlight {
  1841. color: #409eff;
  1842. font-size: 14px;
  1843. font-weight: 700;
  1844. line-height: 1.3;
  1845. }
  1846. .info-divider {
  1847. color: #dcdfe6;
  1848. font-size: 13px;
  1849. margin: 0 4px;
  1850. line-height: 1.3;
  1851. }
  1852. .application-table {
  1853. margin-top: 8px;
  1854. }
  1855. .application-table .el-table {
  1856. width: 100%;
  1857. }
  1858. .application-table .el-table__body-wrapper {
  1859. overflow-x: hidden;
  1860. }
  1861. /* 弹窗内容区域优化 */
  1862. .el-dialog__body {
  1863. padding: 15px 20px 0 20px !important;
  1864. }
  1865. .el-dialog__footer {
  1866. padding: 10px 20px 15px 20px !important;
  1867. }
  1868. .mt-4 {
  1869. margin-top: 16px;
  1870. }
  1871. /* 操作列样式 */
  1872. .action-buttons {
  1873. display: flex;
  1874. align-items: center;
  1875. gap: 2px;
  1876. flex-wrap: wrap;
  1877. justify-content: center;
  1878. .el-button {
  1879. margin: 0;
  1880. padding: 2px 6px;
  1881. font-size: 11px;
  1882. min-height: 20px;
  1883. }
  1884. .status-selector {
  1885. .el-select {
  1886. .el-input {
  1887. font-size: 12px;
  1888. }
  1889. }
  1890. }
  1891. }
  1892. /* 操作列表格样式优化 */
  1893. .el-table {
  1894. .small-padding {
  1895. .cell {
  1896. padding: 8px 4px;
  1897. }
  1898. }
  1899. }
  1900. /* 紧凑表格样式 */
  1901. .compact-table {
  1902. font-size: 13px;
  1903. :deep(.el-table__header) {
  1904. th {
  1905. padding: 4px 0;
  1906. font-size: 13px;
  1907. height: 32px;
  1908. }
  1909. }
  1910. :deep(.el-table__body) {
  1911. td {
  1912. padding: 4px 0;
  1913. font-size: 13px;
  1914. }
  1915. }
  1916. :deep(.cell) {
  1917. padding: 0 4px;
  1918. line-height: 1.3;
  1919. word-break: keep-all;
  1920. white-space: nowrap;
  1921. }
  1922. /* 固定列样式优化 */
  1923. :deep(.el-table__fixed-right) {
  1924. box-shadow: -2px 0 6px rgba(0, 0, 0, 0.1);
  1925. }
  1926. :deep(.el-table__fixed-right-patch) {
  1927. background-color: #f5f7fa;
  1928. }
  1929. /* 确保固定列正常显示 */
  1930. :deep(.el-table__fixed-right .el-table__fixed-body-wrapper) {
  1931. background-color: #fff;
  1932. }
  1933. }
  1934. </style>