index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. <template>
  2. <PageShell>
  3. <div class="evaluation-page">
  4. <div class="evaluation-card">
  5. <EvaluationFilter
  6. :evaluation-name="queryParams.evaluationName || ''"
  7. :grade="queryParams.grade || ''"
  8. :position-type="queryParams.positionType || ''"
  9. :status="queryParams.status || ''"
  10. :date-range="dateRange"
  11. :grade-options="gradeOptions"
  12. :position-type-options="positionTypeOptions"
  13. @search="handleQuery"
  14. @reset="resetQuery"
  15. @update:evaluation-name="queryParams.evaluationName = $event"
  16. @update:grade="queryParams.grade = $event"
  17. @update:position-type="queryParams.positionType = $event"
  18. @update:status="queryParams.status = $event"
  19. @update:date-range="dateRange = $event"
  20. />
  21. <div class="evaluation-toolbar">
  22. <el-button class="evaluation-toolbar-button" @click="handleExport">导出数据</el-button>
  23. <el-button class="evaluation-toolbar-button" @click="getList">刷新</el-button>
  24. <el-button class="evaluation-toolbar-button" @click="handleOpenSyncDialog">同步至员工</el-button>
  25. </div>
  26. <EvaluationTable
  27. :loading="loading"
  28. :list="evaluationList"
  29. :total="total"
  30. :page-num="queryParams.pageNum"
  31. :page-size="queryParams.pageSize"
  32. :grade-options="gradeOptions"
  33. :position-type-options="positionTypeOptions"
  34. :switch-loading-map="switchLoadingMap"
  35. :selected-ids="selectedEvaluationIds"
  36. @update:page-num="queryParams.pageNum = $event"
  37. @update:page-size="queryParams.pageSize = $event"
  38. @update:selected-ids="selectedEvaluationIds = $event"
  39. @pagination="getList"
  40. @toggle-status="handleToggleStatus"
  41. @view-apply-list="handleViewApplyList"
  42. />
  43. </div>
  44. <el-dialog v-model="syncDialog.visible" title="同步测评" width="460px" append-to-body>
  45. <div class="sync-dialog-content">
  46. <div class="sync-dialog-row">
  47. <span class="sync-dialog-label">同步员工</span>
  48. <el-select v-model="syncDialog.employeeIds" multiple collapse-tags collapse-tags-tooltip placeholder="请选择" class="sync-dialog-select" :loading="syncOptionLoading">
  49. <el-option v-for="item in employeeOptions" :key="item.id" :label="item.label" :value="item.id" />
  50. </el-select>
  51. </div>
  52. </div>
  53. <template #footer>
  54. <div class="sync-dialog-footer">
  55. <el-button @click="syncDialog.visible = false">取消</el-button>
  56. <el-button type="primary" :loading="syncLoading" @click="handleConfirmSync">确定</el-button>
  57. </div>
  58. </template>
  59. </el-dialog>
  60. </div>
  61. </PageShell>
  62. </template>
  63. <script setup lang="ts">
  64. import { computed, getCurrentInstance, onMounted, reactive, ref, type ComponentInternalInstance } from 'vue';
  65. import { useRouter } from 'vue-router';
  66. import PageShell from '@/components/PageShell/index.vue';
  67. import { getEvaluationParticipantStats, getEvaluationSyncEmployeeOptions, listEvaluation, syncEvaluationToEmployees, updateEvaluationStatus } from '@/api/main/evaluation';
  68. import { EvaluationSyncEmployeeOption, MainExamEvaluationQuery, MainExamEvaluationVO } from '@/api/main/evaluation/types';
  69. import { getDicts } from '@/api/system/dict/data';
  70. import type { DictDataVO } from '@/api/system/dict/data/types';
  71. import EvaluationFilter from './components/EvaluationFilter.vue';
  72. import EvaluationTable from './components/EvaluationTable.vue';
  73. type FilterOption = { label: string; value: string };
  74. type SyncEmployeeOption = EvaluationSyncEmployeeOption & { label: string };
  75. const router = useRouter();
  76. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  77. const modal = proxy?.$modal as any;
  78. const loading = ref(false);
  79. const total = ref(0);
  80. const evaluationList = ref<MainExamEvaluationVO[]>([]);
  81. const dateRange = ref<[string, string] | []>([]);
  82. const gradeOptions = ref<FilterOption[]>([]);
  83. const positionTypeOptions = ref<FilterOption[]>([]);
  84. const employeeOptions = ref<SyncEmployeeOption[]>([]);
  85. const syncLoading = ref(false);
  86. const syncOptionLoading = ref(false);
  87. const selectedEvaluationIds = ref<Array<string | number>>([]);
  88. const switchLoadingMap = reactive<Record<string, boolean>>({});
  89. const syncDialog = reactive({
  90. visible: false,
  91. employeeIds: [] as Array<string | number>
  92. });
  93. const queryParams = reactive<MainExamEvaluationQuery>({
  94. pageNum: 1,
  95. pageSize: 10,
  96. evaluationName: '',
  97. grade: '',
  98. positionType: '',
  99. status: ''
  100. });
  101. const normalizedQuery = computed(() => ({
  102. ...queryParams,
  103. params: {
  104. beginTime: dateRange.value?.[0] || undefined,
  105. endTime: dateRange.value?.[1] || undefined
  106. }
  107. }));
  108. const mapDictOptions = (list: DictDataVO[] = []): FilterOption[] =>
  109. list.map((item) => ({
  110. label: item.dictLabel,
  111. value: item.dictValue
  112. }));
  113. const mergeFieldOptions = (baseOptions: FilterOption[], rows: MainExamEvaluationVO[], field: 'grade' | 'positionType') => {
  114. const optionMap = new Map(baseOptions.map((item) => [item.value, item]));
  115. rows.forEach((item) => {
  116. const value = item[field];
  117. if (value && !optionMap.has(value)) {
  118. optionMap.set(value, { label: value, value });
  119. }
  120. });
  121. return Array.from(optionMap.values());
  122. };
  123. const loadFilterOptions = async () => {
  124. const [gradeRes, positionTypeRes] = await Promise.allSettled([getDicts('main_position_level'), getDicts('main_position_type')]);
  125. if (gradeRes.status === 'fulfilled') {
  126. gradeOptions.value = mapDictOptions(gradeRes.value.data || []);
  127. }
  128. if (positionTypeRes.status === 'fulfilled') {
  129. positionTypeOptions.value = mapDictOptions(positionTypeRes.value.data || []);
  130. }
  131. };
  132. const loadParticipantStats = async (rows: MainExamEvaluationVO[]) => {
  133. const ids = rows.map((item) => item.id).filter((id): id is string | number => id !== undefined && id !== null);
  134. if (!ids.length) {
  135. return rows.map((item) => ({
  136. ...item,
  137. participantCount: 0,
  138. totalCount: 0
  139. }));
  140. }
  141. const res = await getEvaluationParticipantStats(ids);
  142. const statsMap = res.data || {};
  143. return rows.map((item) => {
  144. const stats = statsMap[String(item.id)] || statsMap[item.id as keyof typeof statsMap];
  145. return {
  146. ...item,
  147. participantCount: Number(stats?.participantCount || 0),
  148. totalCount: Number(stats?.totalCount || 0)
  149. };
  150. });
  151. };
  152. const loadSyncEmployeeOptions = async () => {
  153. syncOptionLoading.value = true;
  154. try {
  155. const res = await getEvaluationSyncEmployeeOptions();
  156. const rows = res.data || [];
  157. employeeOptions.value = rows.map((item) => ({
  158. ...item,
  159. label: [item.name, item.mobile, item.studentNo].filter(Boolean).join(' / ')
  160. }));
  161. } finally {
  162. syncOptionLoading.value = false;
  163. }
  164. };
  165. const getList = async () => {
  166. loading.value = true;
  167. try {
  168. const res = await listEvaluation(normalizedQuery.value);
  169. evaluationList.value = await loadParticipantStats(res.rows || []);
  170. total.value = res.total || 0;
  171. gradeOptions.value = mergeFieldOptions(gradeOptions.value, evaluationList.value, 'grade');
  172. positionTypeOptions.value = mergeFieldOptions(positionTypeOptions.value, evaluationList.value, 'positionType');
  173. } finally {
  174. loading.value = false;
  175. }
  176. };
  177. const handleQuery = () => {
  178. queryParams.pageNum = 1;
  179. getList();
  180. };
  181. const resetQuery = () => {
  182. queryParams.evaluationName = '';
  183. queryParams.grade = '';
  184. queryParams.position = '';
  185. queryParams.positionType = '';
  186. queryParams.status = '';
  187. dateRange.value = [];
  188. queryParams.pageNum = 1;
  189. queryParams.pageSize = 10;
  190. getList();
  191. };
  192. const handleToggleStatus = async (row: MainExamEvaluationVO, value: boolean | string | number) => {
  193. const rowKey = String(row.id);
  194. try {
  195. switchLoadingMap[rowKey] = true;
  196. await updateEvaluationStatus(row.id, value ? '0' : '1');
  197. modal?.msgSuccess('状态更新成功');
  198. await getList();
  199. } finally {
  200. switchLoadingMap[rowKey] = false;
  201. }
  202. };
  203. const handleViewApplyList = (row: MainExamEvaluationVO) => {
  204. router.push({
  205. path: '/evaluation/apply-list',
  206. query: {
  207. evaluationId: String(row.id),
  208. evaluationName: row.evaluationName || '',
  209. backPath: '/evaluation'
  210. }
  211. });
  212. };
  213. const handleExport = () => {
  214. proxy?.download(
  215. '/main/examEvaluation/export',
  216. {
  217. ...normalizedQuery.value
  218. },
  219. `evaluation_${new Date().getTime()}.xlsx`
  220. );
  221. };
  222. const handleOpenSyncDialog = () => {
  223. if (!selectedEvaluationIds.value.length) {
  224. modal?.msgWarning('请先勾选测评');
  225. return;
  226. }
  227. syncDialog.employeeIds = [];
  228. syncDialog.visible = true;
  229. if (!employeeOptions.value.length) {
  230. loadSyncEmployeeOptions();
  231. }
  232. };
  233. const handleConfirmSync = async () => {
  234. if (!syncDialog.employeeIds.length) {
  235. modal?.msgWarning('请选择员工');
  236. return;
  237. }
  238. syncLoading.value = true;
  239. try {
  240. await syncEvaluationToEmployees({
  241. evaluationIds: selectedEvaluationIds.value,
  242. studentIds: syncDialog.employeeIds
  243. });
  244. syncDialog.visible = false;
  245. selectedEvaluationIds.value = [];
  246. modal?.msgSuccess('同步成功');
  247. await getList();
  248. } finally {
  249. syncLoading.value = false;
  250. }
  251. };
  252. onMounted(() => {
  253. loadFilterOptions().finally(() => {
  254. getList();
  255. });
  256. });
  257. </script>
  258. <style scoped lang="scss">
  259. .evaluation-page {
  260. min-height: 100%;
  261. }
  262. .evaluation-card {
  263. display: flex;
  264. flex-direction: column;
  265. min-height: calc(100vh - 180px);
  266. padding: 16px;
  267. border-radius: 8px;
  268. background: #fff;
  269. box-sizing: border-box;
  270. }
  271. .evaluation-toolbar {
  272. display: flex;
  273. align-items: center;
  274. justify-content: flex-end;
  275. gap: 12px;
  276. margin-bottom: 16px;
  277. }
  278. .evaluation-toolbar-button {
  279. color: #303133;
  280. background: #fff;
  281. border-color: #303133;
  282. }
  283. .evaluation-toolbar-button:hover,
  284. .evaluation-toolbar-button:focus {
  285. color: #303133;
  286. background: #fff;
  287. border-color: #303133;
  288. }
  289. .sync-dialog-content {
  290. padding: 10px 4px 24px;
  291. }
  292. .sync-dialog-row {
  293. display: flex;
  294. align-items: center;
  295. gap: 16px;
  296. }
  297. .sync-dialog-label {
  298. width: 56px;
  299. color: #303133;
  300. flex-shrink: 0;
  301. }
  302. .sync-dialog-select {
  303. flex: 1;
  304. }
  305. .sync-dialog-footer {
  306. display: flex;
  307. justify-content: flex-end;
  308. gap: 12px;
  309. }
  310. </style>