index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. <template>
  2. <div class="p-4">
  3. <div class="mb-6 p-4 bg-white rounded-lg shadow-sm flex flex-wrap items-center gap-4">
  4. <el-form :inline="true" :model="searchForm" class="flex-grow flex flex-wrap">
  5. <el-form-item>
  6. <span slot="label" class="font-size-4.5">部门名称:</span>
  7. <el-input
  8. v-model="searchForm.departmentName"
  9. style="width: 240px"
  10. size="default"
  11. placeholder="请输入部门名称"
  12. @clear="handleSearch"
  13. />
  14. </el-form-item>
  15. <el-form-item>
  16. <el-button type="primary" :icon="Search" @click="handleSearch" size="search">查询
  17. </el-button>
  18. <el-button :icon="Refresh" @click="handleResetSearch" size="search">重置</el-button>
  19. </el-form-item>
  20. </el-form>
  21. <div class="flex gap-1" style="justify-content:flex-end">
  22. <el-button type="primary" :icon="Plus" @click="handleAdd" size="search">新增</el-button>
  23. <el-button
  24. type="danger"
  25. :icon="Delete"
  26. @click="handleBatchDelete"
  27. :disabled="selectedIds.length === 0"
  28. size="search"
  29. >批量删除
  30. </el-button
  31. >
  32. <el-button type="success" :icon="Download" @click="exportExcel" size="search">导出</el-button>
  33. <el-upload
  34. class="inline-block ml-2"
  35. action="/api/edepartmentEmployeeStats/importData"
  36. :show-file-list="false"
  37. :on-success="handleUploadSuccess"
  38. :on-error="handleUploadError"
  39. :before-upload="handleBeforeUpload"
  40. :http-request="(options) => importExcel(options.file)"
  41. >
  42. <el-button type="info" :icon="Upload" size="search">导入</el-button>
  43. </el-upload>
  44. </div>
  45. </div>
  46. <!-- Table -->
  47. <div class="bg-white rounded-lg shadow-sm p-4 overflow-x-auto">
  48. <el-table
  49. :data="tableData"
  50. style="width: 100%"
  51. border
  52. @selection-change="handleSelectionChange"
  53. :cell-style="{ fontSize: '14px' }"
  54. :header-cell-style="{ fontSize: '14px' }"
  55. :row-style="{ fontSize: '14px' }"
  56. >
  57. <el-table-column type="selection" width="55" fixed="left"/>
  58. <el-table-column prop="departmentName" label="部门名称" align="center"/>
  59. <el-table-column prop="domesticEmployees" label="内籍员工数量 " align="center"/>
  60. <el-table-column prop="educationDistribution" label="学历分布 " align="center"/>
  61. <el-table-column prop="femaleRatio" label="女工比例" align="center"/>
  62. <el-table-column prop="foreignEmployees" label="外籍员工数量" align="center"/>
  63. <el-table-column prop="maleRatio" label="男工比例" align="center"/>
  64. <el-table-column prop="totalEmployees" label="员工总数" align="center"/>
  65. <el-table-column prop="ageDistribution" label="年龄分布" align="center"/>
  66. <el-table-column label="操作" fixed="right" align="center">
  67. <template #default="{ row }">
  68. <el-button :icon="Edit" size="small" @click="handleEdit(row)">编辑</el-button>
  69. <el-button type="danger" :icon="Delete" size="small" @click="handleDelete(row.id)"
  70. >删除
  71. </el-button
  72. >
  73. </template>
  74. </el-table-column>
  75. </el-table>
  76. <!-- Pagination -->
  77. <div class="mt-4 flex justify-end">
  78. <el-pagination
  79. @size-change="handleSizeChange"
  80. @current-change="handleCurrentChange"
  81. :current-page="pageNum"
  82. :page-sizes="[10, 20, 50, 100]"
  83. :page-size="pageSize"
  84. layout="total, sizes, prev, pager, next, jumper"
  85. :total="total"
  86. />
  87. </div>
  88. </div>
  89. <!-- Add/Edit Dialog -->
  90. <el-dialog
  91. v-model="dialogVisible"
  92. :title="dialogTitle"
  93. width="800px"
  94. :close-on-click-modal="false"
  95. :close-on-press-escape="false"
  96. >
  97. <el-form
  98. :model="formData"
  99. ref="formRef"
  100. label-width="120px"
  101. class="grid grid-cols-1 md:grid-cols-2 gap-4"
  102. >
  103. <el-form-item
  104. label="部门名称"
  105. prop="departmentName"
  106. :rules="[{ required: true, message: '请输入部门名称', trigger: 'blur' }]"
  107. >
  108. <el-input v-model="formData.departmentName" placeholder="请输入部门名称"/>
  109. </el-form-item>
  110. <el-form-item
  111. label="内籍员工数量"
  112. prop="domesticEmployees"
  113. :rules="[{ required: true, message: '请输入内籍员工数量', trigger: 'blur' }]"
  114. >
  115. <el-input
  116. v-model="formData.domesticEmployees"
  117. placeholder="请输入内籍员工数量"
  118. />
  119. </el-form-item>
  120. <el-form-item
  121. label="学历分布"
  122. prop="educationDistribution"
  123. :rules="[{ required: true, message: '请输入学历分布', trigger: 'blur' }]"
  124. >
  125. <el-input
  126. v-model="formData.educationDistribution"
  127. placeholder="请输入学历分布"
  128. />
  129. </el-form-item>
  130. <el-form-item
  131. label="女工比例"
  132. prop="femaleRatio"
  133. :rules="[{ required: true, message: '请输入女工比例', trigger: 'blur' }]"
  134. >
  135. <el-input
  136. v-model="formData.femaleRatio"
  137. placeholder="请输入女工比例"
  138. />
  139. </el-form-item>
  140. <el-form-item
  141. label="外籍员工数量"
  142. prop="foreignEmployees"
  143. :rules="[
  144. { required: true, message: '请输入外籍员工数量', trigger: 'blur' },
  145. { type: 'number', message: '请输入数字', trigger: 'blur' },
  146. ]"
  147. >
  148. <el-input v-model.number="formData.foreignEmployees" placeholder="请输入外籍员工数量"/>
  149. </el-form-item>
  150. <el-form-item
  151. label="男工比例"
  152. prop="maleRatio"
  153. :rules="[
  154. { required: true, message: '请输入男工比例', trigger: 'blur' },
  155. { type: 'number', message: '请输入数字', trigger: 'blur' },
  156. ]"
  157. >
  158. <el-input v-model.number="formData.maleRatio" placeholder="请输入男工比例"/>
  159. </el-form-item>
  160. <el-form-item
  161. label="员工总数"
  162. prop="totalEmployees"
  163. :rules="[
  164. { required: true, message: '请输入员工总数', trigger: 'blur' },
  165. { type: 'number', message: '请输入数字', trigger: 'blur' },
  166. ]"
  167. >
  168. <el-input v-model.number="formData.totalEmployees" placeholder="请输入员工总数"/>
  169. </el-form-item>
  170. <el-form-item
  171. label="年龄分布"
  172. prop="ageDistribution"
  173. :rules="[
  174. { required: true, message: '请输入年龄分布', trigger: 'blur' },
  175. { type: 'number', message: '请输入数字', trigger: 'blur' },
  176. ]"
  177. >
  178. <el-input v-model.number="formData.ageDistribution" placeholder="请输入年龄分布"/>
  179. </el-form-item>
  180. </el-form>
  181. <template #footer>
  182. <span class="dialog-footer">
  183. <el-button @click="dialogVisible = false">取消</el-button>
  184. <el-button type="primary" @click="handleConfirm">确定</el-button>
  185. </span>
  186. </template>
  187. </el-dialog>
  188. </div>
  189. </template>
  190. <script setup lang="ts">
  191. import {onMounted, reactive, ref} from 'vue'
  192. import {clientDownloadExcel, clientGet, clientPost} from '@/utils/request.ts'
  193. import type {FormInstance, UploadProps} from 'element-plus'
  194. import {ElForm, ElLoading, ElMessage, ElMessageBox} from 'element-plus'
  195. import {Delete, Download, Edit, Plus, Refresh, Search, Upload} from '@element-plus/icons-vue'
  196. // 假设 BaseResponse 和 PageType 在全局或 request.ts 中已定义
  197. interface BaseResponse {
  198. code: number
  199. msg: string
  200. }
  201. interface PageType<T> {
  202. records: T[]
  203. total: number
  204. size: number
  205. current: number
  206. pages: number
  207. }
  208. interface Property {
  209. id: string // 主键ID
  210. departmentName: string // 部门名称
  211. domesticEmployees: string // 内籍员工数量
  212. educationDistribution: string // 学历分布
  213. femaleRatio: string // 女工比例
  214. foreignEmployees: string // 外籍员工数量
  215. maleRatio: string // 男工比例
  216. totalEmployees: string // 员工总数
  217. ageDistribution: string // 年龄分布
  218. }
  219. interface PropertyOneResponse extends BaseResponse {
  220. data: Property
  221. }
  222. interface PropertyListResponse extends BaseResponse {
  223. data: PageType<Property>
  224. }
  225. interface AddProperty {
  226. departmentName: string // 部门名称
  227. domesticEmployees: string // 内籍员工数量
  228. educationDistribution: string // 学历分布
  229. femaleRatio: string // 女工比例
  230. foreignEmployees: string // 外籍员工数量
  231. maleRatio: string // 男工比例
  232. totalEmployees: string // 员工总数
  233. ageDistribution: string // 年龄分布
  234. }
  235. interface UpdateProperty {
  236. id?: string
  237. departmentName: string // 部门名称
  238. domesticEmployees: string // 内籍员工数量
  239. educationDistribution: string // 学历分布
  240. femaleRatio: string // 女工比例
  241. foreignEmployees: string // 外籍员工数量
  242. maleRatio: string // 男工比例
  243. totalEmployees: string // 员工总数
  244. ageDistribution: string // 年龄分布
  245. }
  246. // 响应式状态变量
  247. const tableData = ref<Property[]>([])
  248. const total = ref(0)
  249. const pageSize = ref(10)
  250. const pageNum = ref(1)
  251. const searchForm = reactive({
  252. departmentName: ''
  253. })
  254. const dialogVisible = ref(false)
  255. const dialogTitle = ref('')
  256. const isEdit = ref(false)
  257. const formData = reactive<Property>({})
  258. const formRef = ref<FormInstance>()
  259. const selectedIds = ref<string[]>([])
  260. // 规上企业是否
  261. const arrearsOptions = [
  262. {label: '是', value: '是'},
  263. {label: '否', value: '否'},
  264. ]
  265. // 年度选项(动态生成最近5年)
  266. const currentYear = new Date().getFullYear()
  267. // 新增
  268. const add = async (data: AddProperty) => {
  269. const res = await clientPost<AddProperty, BaseResponse>('/edepartmentEmployeeStats/save', {
  270. ...data,
  271. })
  272. if (res.code !== 200) {
  273. ElMessage.error(res.msg)
  274. return false
  275. }
  276. ElMessage.success(res.msg)
  277. return true
  278. }
  279. // 根据id获取数据
  280. const getById = async (id: string) => {
  281. const res = await clientGet<null, PropertyOneResponse>('/edepartmentEmployeeStats/getById/' + id)
  282. if (res.code !== 200) {
  283. ElMessage.error(res.msg)
  284. return null
  285. }
  286. return res.data
  287. }
  288. // 分页获取数据
  289. const getList = async () => {
  290. // 构建参数对象,只包含有值的搜索条件
  291. const params = {
  292. pageNum: pageNum.value,
  293. pageSize: pageSize.value,
  294. ...(searchForm.departmentName ? {departmentName: searchForm.departmentName} : {})
  295. }
  296. const res = await clientGet<{
  297. pageNum: number
  298. pageSize: number
  299. departmentName?: string
  300. },
  301. PropertyListResponse>('/edepartmentEmployeeStats/findByPage', {
  302. params,
  303. })
  304. if (res.code !== 200) {
  305. ElMessage.error(res.msg)
  306. return
  307. }
  308. tableData.value = res.data.records
  309. total.value = res.data.total
  310. }
  311. // 批量删除
  312. const delBatch = async (ids: string[]) => {
  313. const res = await clientPost<string[], BaseResponse>('/edepartmentEmployeeStats/deleteBatch', ids)
  314. if (res.code !== 200) {
  315. ElMessage.error(res.msg)
  316. return false
  317. }
  318. ElMessage.success(res.msg)
  319. return true
  320. }
  321. // 修改
  322. const update = async (data: UpdateProperty) => {
  323. const res = await clientPost<UpdateProperty, BaseResponse>('/edepartmentEmployeeStats/update', {
  324. ...data,
  325. })
  326. if (res.code !== 200) {
  327. ElMessage.error(res.msg)
  328. return false
  329. }
  330. ElMessage.success(res.msg)
  331. return true
  332. }
  333. // 导出为excel
  334. const exportExcel = async () => {
  335. const loading = ElLoading.service({
  336. lock: true,
  337. text: '导出中,请稍候...',
  338. background: 'rgba(0, 0, 0, 0.1)',
  339. fullscreen: true,
  340. })
  341. try {
  342. // 构建参数对象,只包含有值的搜索条件
  343. const params = {
  344. ...(searchForm.departmentName ? {departmentName: searchForm.departmentName} : {})
  345. }
  346. await clientDownloadExcel('/edepartmentEmployeeStats/exportData', {
  347. params,
  348. })
  349. ElMessage.success('导出成功')
  350. } catch (error) {
  351. console.error('导出失败:', error)
  352. ElMessage.error('导出失败')
  353. } finally {
  354. loading.close()
  355. }
  356. }
  357. // excel导入
  358. const importExcel = async (file: File) => {
  359. const formData = new FormData()
  360. formData.append('file', file)
  361. const res = await clientPost<FormData, BaseResponse>(
  362. '/edepartmentEmployeeStats/importData',
  363. formData,
  364. {
  365. headers: {
  366. 'Content-Type': 'multipart/form-data',
  367. },
  368. },
  369. )
  370. if (res.code !== 200) {
  371. ElMessage.error(res.msg)
  372. return false
  373. }
  374. ElMessage.success(res.msg)
  375. getList()
  376. return true
  377. }
  378. // --- CRUD 操作和处理函数 ---
  379. const handleAdd = () => {
  380. isEdit.value = false
  381. dialogTitle.value = '新增用企业信息'
  382. // 重置表单数据
  383. Object.keys(formData).forEach((key) => delete formData[key as keyof Property])
  384. // 设置默认值为当前年度
  385. formData.openingDate = `${currentYear}`
  386. dialogVisible.value = true
  387. }
  388. const handleEdit = async (row: Property) => {
  389. isEdit.value = true
  390. dialogTitle.value = '编辑企业信息'
  391. const data = await getById(row.id)
  392. if (data) {
  393. Object.assign(formData, data)
  394. dialogVisible.value = true
  395. }
  396. }
  397. const handleDelete = async (id: string) => {
  398. ElMessageBox.confirm('确定删除此条记录吗?', '提示', {
  399. confirmButtonText: '确定',
  400. cancelButtonText: '取消',
  401. type: 'warning',
  402. })
  403. .then(async () => {
  404. const success = await delBatch([id])
  405. if (success) {
  406. getList()
  407. }
  408. })
  409. .catch(() => {
  410. ElMessage.info('已取消删除')
  411. })
  412. }
  413. const handleBatchDelete = () => {
  414. if (selectedIds.value.length === 0) {
  415. ElMessage.warning('请选择要删除的记录')
  416. return
  417. }
  418. ElMessageBox.confirm(`确定删除选中的 ${selectedIds.value.length} 条记录吗?`, '提示', {
  419. confirmButtonText: '确定',
  420. cancelButtonText: '取消',
  421. type: 'warning',
  422. })
  423. .then(async () => {
  424. const success = await delBatch(selectedIds.value)
  425. if (success) {
  426. getList()
  427. }
  428. })
  429. .catch(() => {
  430. ElMessage.info('已取消批量删除')
  431. })
  432. }
  433. const handleSearch = () => {
  434. pageNum.value = 1 // 搜索时重置到第一页
  435. getList()
  436. }
  437. const handleResetSearch = () => {
  438. searchForm.departmentName = ''
  439. pageNum.value = 1
  440. getList()
  441. }
  442. const handleConfirm = async () => {
  443. if (!formRef.value) return
  444. await formRef.value.validate(async (valid) => {
  445. if (valid) {
  446. let success = false
  447. const dataToSend = {...formData}
  448. if (isEdit.value) {
  449. success = await update(dataToSend)
  450. } else {
  451. success = await add(dataToSend)
  452. }
  453. if (success) {
  454. dialogVisible.value = false
  455. getList()
  456. }
  457. } else {
  458. ElMessage.error('请检查表单填写')
  459. }
  460. })
  461. }
  462. const handleCurrentChange = (val: number) => {
  463. pageNum.value = val
  464. getList()
  465. }
  466. const handleSizeChange = (val: number) => {
  467. pageSize.value = val
  468. pageNum.value = 1 // 页大小改变时重置到第一页
  469. getList()
  470. }
  471. const handleSelectionChange = (selection: Property[]) => {
  472. selectedIds.value = selection.map((item) => item.id)
  473. }
  474. const handleUploadSuccess: UploadProps['onSuccess'] = async (response, uploadFile) => {
  475. if (response && response.code === 200) {
  476. ElMessage.success('文件导入成功')
  477. getList()
  478. } else {
  479. ElMessage.error(`文件导入失败: ${response?.msg || '未知错误'}`)
  480. }
  481. }
  482. const handleUploadError: UploadProps['onError'] = (error) => {
  483. ElMessage.error(`文件导入失败: ${error.message}`)
  484. }
  485. const handleBeforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
  486. const isExcel =
  487. rawFile.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
  488. rawFile.type === 'application/vnd.ms-excel'
  489. if (!isExcel) {
  490. ElMessage.error('导入文件只能是 Excel 格式!')
  491. return false
  492. }
  493. return true
  494. }
  495. const init = () => {
  496. getList()
  497. };
  498. onMounted(() => {
  499. init()
  500. })
  501. </script>
  502. <style scoped>
  503. /* Add any specific styles here if needed, otherwise UnoCSS handles most */
  504. </style>