| New file |
| | |
| | | package com.rongyichuang.employee.dto.response; |
| | | |
| | | /** |
| | | * 员工审核列表条目 |
| | | */ |
| | | public class EmployeeReviewApplicationResponse { |
| | | private Long id; |
| | | private String playerName; |
| | | private String projectName; |
| | | private String activityName; |
| | | private Integer state; |
| | | private String stateText; |
| | | private String stateType; |
| | | private String applyTime; |
| | | |
| | | public EmployeeReviewApplicationResponse() { |
| | | } |
| | | |
| | | public Long getId() { |
| | | return id; |
| | | } |
| | | |
| | | public void setId(Long id) { |
| | | this.id = id; |
| | | } |
| | | |
| | | public String getPlayerName() { |
| | | return playerName; |
| | | } |
| | | |
| | | public void setPlayerName(String playerName) { |
| | | this.playerName = playerName; |
| | | } |
| | | |
| | | public String getProjectName() { |
| | | return projectName; |
| | | } |
| | | |
| | | public void setProjectName(String projectName) { |
| | | this.projectName = projectName; |
| | | } |
| | | |
| | | public String getActivityName() { |
| | | return activityName; |
| | | } |
| | | |
| | | public void setActivityName(String activityName) { |
| | | this.activityName = activityName; |
| | | } |
| | | |
| | | public Integer getState() { |
| | | return state; |
| | | } |
| | | |
| | | public void setState(Integer state) { |
| | | this.state = state; |
| | | } |
| | | |
| | | public String getStateText() { |
| | | return stateText; |
| | | } |
| | | |
| | | public void setStateText(String stateText) { |
| | | this.stateText = stateText; |
| | | } |
| | | |
| | | public String getStateType() { |
| | | return stateType; |
| | | } |
| | | |
| | | public void setStateType(String stateType) { |
| | | this.stateType = stateType; |
| | | } |
| | | |
| | | public String getApplyTime() { |
| | | return applyTime; |
| | | } |
| | | |
| | | public void setApplyTime(String applyTime) { |
| | | this.applyTime = applyTime; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.rongyichuang.employee.dto.response; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 员工审核分页结果 |
| | | */ |
| | | public class EmployeeReviewPageResponse { |
| | | private List<EmployeeReviewApplicationResponse> content; |
| | | private int totalElements; |
| | | private int page; |
| | | private int size; |
| | | |
| | | public EmployeeReviewPageResponse() { |
| | | } |
| | | |
| | | public EmployeeReviewPageResponse(List<EmployeeReviewApplicationResponse> content, int totalElements, int page, int size) { |
| | | this.content = content; |
| | | this.totalElements = totalElements; |
| | | this.page = page; |
| | | this.size = size; |
| | | } |
| | | |
| | | public List<EmployeeReviewApplicationResponse> getContent() { |
| | | return content; |
| | | } |
| | | |
| | | public void setContent(List<EmployeeReviewApplicationResponse> content) { |
| | | this.content = content; |
| | | } |
| | | |
| | | public int getTotalElements() { |
| | | return totalElements; |
| | | } |
| | | |
| | | public void setTotalElements(int totalElements) { |
| | | this.totalElements = totalElements; |
| | | } |
| | | |
| | | public int getPage() { |
| | | return page; |
| | | } |
| | | |
| | | public void setPage(int page) { |
| | | this.page = page; |
| | | } |
| | | |
| | | public int getSize() { |
| | | return size; |
| | | } |
| | | |
| | | public void setSize(int size) { |
| | | this.size = size; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.rongyichuang.employee.dto.response; |
| | | |
| | | /** |
| | | * 员工审核统计数据 |
| | | */ |
| | | public class EmployeeReviewStatsResponse { |
| | | private int pendingCount; |
| | | private int approvedCount; |
| | | private int rejectedCount; |
| | | |
| | | public EmployeeReviewStatsResponse() { |
| | | } |
| | | |
| | | public EmployeeReviewStatsResponse(int pendingCount, int approvedCount, int rejectedCount) { |
| | | this.pendingCount = pendingCount; |
| | | this.approvedCount = approvedCount; |
| | | this.rejectedCount = rejectedCount; |
| | | } |
| | | |
| | | public int getPendingCount() { |
| | | return pendingCount; |
| | | } |
| | | |
| | | public void setPendingCount(int pendingCount) { |
| | | this.pendingCount = pendingCount; |
| | | } |
| | | |
| | | public int getApprovedCount() { |
| | | return approvedCount; |
| | | } |
| | | |
| | | public void setApprovedCount(int approvedCount) { |
| | | this.approvedCount = approvedCount; |
| | | } |
| | | |
| | | public int getRejectedCount() { |
| | | return rejectedCount; |
| | | } |
| | | |
| | | public void setRejectedCount(int rejectedCount) { |
| | | this.rejectedCount = rejectedCount; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.rongyichuang.employee.resolver; |
| | | |
| | | import com.rongyichuang.employee.dto.response.EmployeeReviewPageResponse; |
| | | import com.rongyichuang.employee.dto.response.EmployeeReviewStatsResponse; |
| | | import com.rongyichuang.employee.service.EmployeeReviewService; |
| | | import org.springframework.graphql.data.method.annotation.Argument; |
| | | import org.springframework.graphql.data.method.annotation.QueryMapping; |
| | | import org.springframework.stereotype.Controller; |
| | | |
| | | /** |
| | | * 员工审核GraphQL接口 |
| | | */ |
| | | @Controller |
| | | public class EmployeeReviewResolver { |
| | | |
| | | private final EmployeeReviewService employeeReviewService; |
| | | |
| | | public EmployeeReviewResolver(EmployeeReviewService employeeReviewService) { |
| | | this.employeeReviewService = employeeReviewService; |
| | | } |
| | | |
| | | @QueryMapping |
| | | public EmployeeReviewStatsResponse employeeReviewStats(@Argument String keyword) { |
| | | return employeeReviewService.getReviewStats(keyword); |
| | | } |
| | | |
| | | @QueryMapping |
| | | public EmployeeReviewPageResponse employeeReviewApplications(@Argument String keyword, |
| | | @Argument Integer state, |
| | | @Argument Integer page, |
| | | @Argument Integer size) { |
| | | return employeeReviewService.listReviewApplications(keyword, state, page, size); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.rongyichuang.employee.service; |
| | | |
| | | import com.rongyichuang.common.exception.BusinessException; |
| | | import com.rongyichuang.common.util.UserContextUtil; |
| | | import com.rongyichuang.employee.dto.response.EmployeeReviewApplicationResponse; |
| | | import com.rongyichuang.employee.dto.response.EmployeeReviewPageResponse; |
| | | import com.rongyichuang.employee.dto.response.EmployeeReviewStatsResponse; |
| | | import jakarta.persistence.EntityManager; |
| | | import jakarta.persistence.PersistenceContext; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.sql.Timestamp; |
| | | import java.time.LocalDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 员工审核数据服务 |
| | | */ |
| | | @Service |
| | | public class EmployeeReviewService { |
| | | |
| | | private static final Logger log = LoggerFactory.getLogger(EmployeeReviewService.class); |
| | | |
| | | private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); |
| | | |
| | | @PersistenceContext |
| | | private EntityManager entityManager; |
| | | |
| | | private final UserContextUtil userContextUtil; |
| | | private final EmployeeService employeeService; |
| | | |
| | | public EmployeeReviewService(UserContextUtil userContextUtil, EmployeeService employeeService) { |
| | | this.userContextUtil = userContextUtil; |
| | | this.employeeService = employeeService; |
| | | } |
| | | |
| | | /** |
| | | * 获取员工审核统计 |
| | | */ |
| | | public EmployeeReviewStatsResponse getReviewStats(String keyword) { |
| | | ensureEmployeeIdentity(); |
| | | |
| | | StringBuilder sql = new StringBuilder(); |
| | | sql.append("SELECT COALESCE(ap.state, 0) AS state, COUNT(*) AS total "); |
| | | sql.append("FROM t_activity_player ap "); |
| | | sql.append("JOIN t_player p ON p.id = ap.player_id "); |
| | | sql.append("JOIN t_activity stage ON stage.id = ap.stage_id "); |
| | | sql.append("JOIN t_activity parent ON parent.id = stage.pid "); |
| | | sql.append("WHERE stage.sort_order = 1 "); |
| | | |
| | | if (StringUtils.hasText(keyword)) { |
| | | sql.append("AND (p.name LIKE :keyword OR parent.name LIKE :keyword OR ap.project_name LIKE :keyword) "); |
| | | } |
| | | |
| | | sql.append("GROUP BY ap.state"); |
| | | |
| | | var query = entityManager.createNativeQuery(sql.toString()); |
| | | if (StringUtils.hasText(keyword)) { |
| | | query.setParameter("keyword", wrapKeyword(keyword)); |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | List<Object[]> rows = query.getResultList(); |
| | | |
| | | int pending = 0; |
| | | int approved = 0; |
| | | int rejected = 0; |
| | | |
| | | for (Object[] row : rows) { |
| | | int state = 0; |
| | | int total = 0; |
| | | if (row[0] instanceof Number) { |
| | | state = ((Number) row[0]).intValue(); |
| | | } else if (row[0] != null) { |
| | | state = Integer.parseInt(row[0].toString()); |
| | | } |
| | | if (row[1] instanceof Number) { |
| | | total = ((Number) row[1]).intValue(); |
| | | } else if (row[1] != null) { |
| | | total = Integer.parseInt(row[1].toString()); |
| | | } |
| | | |
| | | switch (state) { |
| | | case 0 -> pending = total; |
| | | case 1 -> approved = total; |
| | | case 2 -> rejected = total; |
| | | default -> log.debug("忽略状态{}的统计", state); |
| | | } |
| | | } |
| | | |
| | | return new EmployeeReviewStatsResponse(pending, approved, rejected); |
| | | } |
| | | |
| | | /** |
| | | * 分页查询员工审核列表 |
| | | */ |
| | | public EmployeeReviewPageResponse listReviewApplications(String keyword, Integer state, Integer page, Integer size) { |
| | | ensureEmployeeIdentity(); |
| | | |
| | | int pageNumber = (page != null && page > 0) ? page : 1; |
| | | int pageSize = (size != null && size > 0) ? size : 10; |
| | | int offset = (pageNumber - 1) * pageSize; |
| | | |
| | | String baseSql = "SELECT ap.id, p.name AS player_name, ap.project_name, parent.name AS activity_name, ap.state, ap.create_time " + |
| | | "FROM t_activity_player ap " + |
| | | "JOIN t_player p ON p.id = ap.player_id " + |
| | | "JOIN t_activity stage ON stage.id = ap.stage_id " + |
| | | "JOIN t_activity parent ON parent.id = stage.pid "; |
| | | |
| | | StringBuilder whereClause = new StringBuilder(); |
| | | whereClause.append("stage.sort_order = 1"); |
| | | |
| | | if (StringUtils.hasText(keyword)) { |
| | | whereClause.append(" AND (p.name LIKE :keyword OR parent.name LIKE :keyword OR ap.project_name LIKE :keyword)"); |
| | | } |
| | | |
| | | if (state != null) { |
| | | whereClause.append(" AND ap.state = :state"); |
| | | } |
| | | |
| | | String where = whereClause.length() > 0 ? " WHERE " + whereClause + " " : ""; |
| | | String order = "ORDER BY ap.create_time DESC "; |
| | | String limit = "LIMIT " + pageSize + " OFFSET " + offset + " "; |
| | | |
| | | var query = entityManager.createNativeQuery(baseSql + where + order + limit); |
| | | if (StringUtils.hasText(keyword)) { |
| | | query.setParameter("keyword", wrapKeyword(keyword)); |
| | | } |
| | | if (state != null) { |
| | | query.setParameter("state", state); |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | List<Object[]> rows = query.getResultList(); |
| | | List<EmployeeReviewApplicationResponse> content = new ArrayList<>(); |
| | | |
| | | for (Object[] row : rows) { |
| | | EmployeeReviewApplicationResponse dto = new EmployeeReviewApplicationResponse(); |
| | | dto.setId(row[0] instanceof Number ? ((Number) row[0]).longValue() : parseLong(row[0])); |
| | | dto.setPlayerName(row[1] != null ? row[1].toString() : ""); |
| | | dto.setProjectName(row[2] != null ? row[2].toString() : ""); |
| | | dto.setActivityName(row[3] != null ? row[3].toString() : ""); |
| | | Integer reviewState = row[4] instanceof Number ? ((Number) row[4]).intValue() : parseInt(row[4]); |
| | | dto.setState(reviewState); |
| | | dto.setStateText(resolveStateText(reviewState)); |
| | | dto.setStateType(resolveStateType(reviewState)); |
| | | dto.setApplyTime(formatDateTime(row[5])); |
| | | content.add(dto); |
| | | } |
| | | |
| | | String countSql = "SELECT COUNT(*) FROM t_activity_player ap " + |
| | | "JOIN t_player p ON p.id = ap.player_id " + |
| | | "JOIN t_activity stage ON stage.id = ap.stage_id " + |
| | | "JOIN t_activity parent ON parent.id = stage.pid " + |
| | | where; |
| | | |
| | | var countQuery = entityManager.createNativeQuery(countSql); |
| | | if (StringUtils.hasText(keyword)) { |
| | | countQuery.setParameter("keyword", wrapKeyword(keyword)); |
| | | } |
| | | if (state != null) { |
| | | countQuery.setParameter("state", state); |
| | | } |
| | | |
| | | Number totalNumber = (Number) countQuery.getSingleResult(); |
| | | int total = totalNumber != null ? totalNumber.intValue() : 0; |
| | | |
| | | return new EmployeeReviewPageResponse(content, total, pageNumber, pageSize); |
| | | } |
| | | |
| | | private void ensureEmployeeIdentity() { |
| | | Long userId = userContextUtil.getCurrentUserId(); |
| | | if (userId == null) { |
| | | throw new BusinessException("UNAUTHORIZED", "请先登录"); |
| | | } |
| | | if (employeeService.findByUserId(userId) == null) { |
| | | throw new BusinessException("EMPLOYEE_REQUIRED", "当前用户没有审核权限"); |
| | | } |
| | | } |
| | | |
| | | private String wrapKeyword(String keyword) { |
| | | return "%" + keyword.trim() + "%"; |
| | | } |
| | | |
| | | private Integer parseInt(Object value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | try { |
| | | return Integer.parseInt(value.toString()); |
| | | } catch (NumberFormatException ex) { |
| | | log.warn("无法解析整型值: {}", value, ex); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private Long parseLong(Object value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | try { |
| | | return Long.parseLong(value.toString()); |
| | | } catch (NumberFormatException ex) { |
| | | log.warn("无法解析长整型值: {}", value, ex); |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private String formatDateTime(Object value) { |
| | | if (value instanceof Timestamp timestamp) { |
| | | LocalDateTime dateTime = timestamp.toLocalDateTime(); |
| | | return DATE_TIME_FORMATTER.format(dateTime); |
| | | } |
| | | if (value instanceof LocalDateTime localDateTime) { |
| | | return DATE_TIME_FORMATTER.format(localDateTime); |
| | | } |
| | | return value != null ? value.toString() : ""; |
| | | } |
| | | |
| | | private String resolveStateText(Integer state) { |
| | | if (state == null) { |
| | | return "未知"; |
| | | } |
| | | return switch (state) { |
| | | case 0 -> "未审核"; |
| | | case 1 -> "审核通过"; |
| | | case 2 -> "审核驳回"; |
| | | default -> "未知"; |
| | | }; |
| | | } |
| | | |
| | | private String resolveStateType(Integer state) { |
| | | if (state == null) { |
| | | return "info"; |
| | | } |
| | | return switch (state) { |
| | | case 0 -> "warning"; |
| | | case 1 -> "success"; |
| | | case 2 -> "danger"; |
| | | default -> "info"; |
| | | }; |
| | | } |
| | | } |
| | |
| | | # Spring Data的Page对象会自动映射到GraphQL |
| | | |
| | | # 扩展查询类型 |
| | | |
| | | |
| | | type EmployeeReviewApplication { |
| | | id: Long! |
| | | playerName: String |
| | | projectName: String |
| | | activityName: String |
| | | state: Int |
| | | stateText: String |
| | | stateType: String |
| | | applyTime: String |
| | | } |
| | | |
| | | type EmployeeReviewPage { |
| | | content: [EmployeeReviewApplication!]! |
| | | totalElements: Int! |
| | | page: Int! |
| | | size: Int! |
| | | } |
| | | |
| | | type EmployeeReviewStats { |
| | | pendingCount: Int! |
| | | approvedCount: Int! |
| | | rejectedCount: Int! |
| | | } |
| | | |
| | | extend type Query { |
| | | # 获取所有员工列表 |
| | | employees: [EmployeeResponse!]! |
| | |
| | | |
| | | # 根据ID获取员工详情 |
| | | employee(id: Long!): EmployeeResponse |
| | | |
| | | # 员工审核统计 |
| | | employeeReviewStats(keyword: String): EmployeeReviewStats! |
| | | |
| | | # 员工审核列表 |
| | | employeeReviewApplications(keyword: String, state: Int, page: Int, size: Int): EmployeeReviewPage! |
| | | } |
| | | |
| | | # 扩展变更类型 |
| | |
| | | "pages/registration/registration", |
| | | "pages/profile/profile", |
| | | "pages/profile/personal-info", |
| | | "pages/profile/employee-review", |
| | | "pages/profile/employee-review-detail", |
| | | "pages/project/detail", |
| | | "pages/message/message", |
| | | "pages/review/index", |
| | |
| | | "navigationBarTextStyle": "black", |
| | | "backgroundColor": "#f5f5f5" |
| | | }, |
| | | |
| | | "tabBar": { |
| | | "custom": true, |
| | | "color": "#999999", |
| | |
| | | } |
| | | ] |
| | | }, |
| | | |
| | | "networkTimeout": { |
| | | "request": 10000, |
| | | "downloadFile": 10000 |
| | |
| | | "desc": "你的位置信息将用于小程序位置接口的效果展示" |
| | | } |
| | | }, |
| | | "requiredBackgroundModes": ["audio"], |
| | | "requiredBackgroundModes": [ |
| | | "audio" |
| | | ], |
| | | "sitemapLocation": "sitemap.json" |
| | | } |
| New file |
| | |
| | | const { graphqlRequest } = require('../../lib/utils') |
| | | |
| | | const DETAIL_QUERY = ` |
| | | query ActivityPlayerDetail($id: ID!) { |
| | | activityPlayerDetail(id: $id) { |
| | | id |
| | | activityName |
| | | projectName |
| | | description |
| | | feedback |
| | | state |
| | | playerInfo { |
| | | id |
| | | name |
| | | phone |
| | | } |
| | | submissionFiles { |
| | | id |
| | | name |
| | | fullUrl |
| | | fullThumbUrl |
| | | fileExt |
| | | mediaType |
| | | } |
| | | } |
| | | } |
| | | ` |
| | | |
| | | const APPROVE_MUTATION = ` |
| | | mutation ApproveActivityPlayer($id: ID!, $feedback: String) { |
| | | approveActivityPlayer(activityPlayerId: $id, feedback: $feedback) |
| | | } |
| | | ` |
| | | |
| | | const REJECT_MUTATION = ` |
| | | mutation RejectActivityPlayer($id: ID!, $feedback: String) { |
| | | rejectActivityPlayer(activityPlayerId: $id, feedback: $feedback) |
| | | } |
| | | ` |
| | | |
| | | Page({ |
| | | data: { |
| | | loading: false, |
| | | submitting: false, |
| | | activityPlayerId: null, |
| | | detail: null, |
| | | feedback: '' |
| | | }, |
| | | |
| | | onLoad(options) { |
| | | if (options && options.id) { |
| | | if (typeof this.getOpenerEventChannel === 'function') { |
| | | this.eventChannel = this.getOpenerEventChannel() |
| | | } |
| | | this.setData({ activityPlayerId: options.id }) |
| | | this.loadDetail() |
| | | } else { |
| | | wx.showToast({ title: '缺少报名ID', icon: 'none' }) |
| | | setTimeout(() => wx.navigateBack(), 800) |
| | | } |
| | | }, |
| | | |
| | | async loadDetail() { |
| | | if (!this.data.activityPlayerId) { |
| | | return |
| | | } |
| | | |
| | | this.setData({ loading: true }) |
| | | |
| | | try { |
| | | const result = await graphqlRequest(DETAIL_QUERY, { id: this.data.activityPlayerId }) |
| | | const detail = result && result.activityPlayerDetail |
| | | |
| | | if (!detail) { |
| | | wx.showToast({ title: '未找到报名信息', icon: 'none' }) |
| | | return |
| | | } |
| | | |
| | | this.setData({ |
| | | detail: { |
| | | ...detail, |
| | | stateText: this.getStateText(detail.state), |
| | | submissionFiles: (detail.submissionFiles || []).map(file => ({ |
| | | id: file.id, |
| | | name: file.name, |
| | | url: file.fullUrl || file.fullThumbUrl || '' |
| | | })) |
| | | }, |
| | | feedback: detail.feedback || '' |
| | | }) |
| | | } catch (error) { |
| | | console.error('加载审核详情失败:', error) |
| | | wx.showToast({ title: '加载失败', icon: 'none' }) |
| | | } finally { |
| | | this.setData({ loading: false }) |
| | | } |
| | | }, |
| | | |
| | | onFeedbackInput(e) { |
| | | this.setData({ feedback: e.detail.value }) |
| | | }, |
| | | |
| | | onApprove() { |
| | | this.handleAudit('APPROVE') |
| | | }, |
| | | |
| | | onReject() { |
| | | this.handleAudit('REJECT') |
| | | }, |
| | | |
| | | async handleAudit(action) { |
| | | if (!this.data.activityPlayerId) { |
| | | return |
| | | } |
| | | |
| | | const mutation = action === 'APPROVE' ? APPROVE_MUTATION : REJECT_MUTATION |
| | | const successText = action === 'APPROVE' ? '审核通过' : '已驳回' |
| | | |
| | | this.setData({ submitting: true }) |
| | | |
| | | try { |
| | | const variables = { |
| | | id: Number(this.data.activityPlayerId), |
| | | feedback: this.data.feedback ? this.data.feedback.trim() : null |
| | | } |
| | | const result = await graphqlRequest(mutation, variables) |
| | | const success = result && (action === 'APPROVE' ? result.approveActivityPlayer : result.rejectActivityPlayer) |
| | | |
| | | if (success) { |
| | | wx.showToast({ title: successText, icon: 'success' }) |
| | | if (this.eventChannel) { |
| | | this.eventChannel.emit('auditUpdated', { id: this.data.activityPlayerId }) |
| | | } |
| | | setTimeout(() => wx.navigateBack(), 600) |
| | | } else { |
| | | wx.showToast({ title: '操作失败', icon: 'none' }) |
| | | } |
| | | } catch (error) { |
| | | console.error('提交审核决策失败:', error) |
| | | wx.showToast({ title: '操作失败', icon: 'none' }) |
| | | } finally { |
| | | this.setData({ submitting: false }) |
| | | } |
| | | }, |
| | | |
| | | previewFile(e) { |
| | | const url = e.currentTarget.dataset.url |
| | | if (url) { |
| | | wx.navigateTo({ url: `/pages/webview/webview?url=${encodeURIComponent(url)}` }) |
| | | } |
| | | }, |
| | | |
| | | getStateText(state) { |
| | | switch (state) { |
| | | case 0: |
| | | return '未审核' |
| | | case 1: |
| | | return '审核通过' |
| | | case 2: |
| | | return '审核驳回' |
| | | default: |
| | | return '未知状态' |
| | | } |
| | | } |
| | | }) |
| New file |
| | |
| | | { |
| | | "navigationBarTitleText": "审核详情", |
| | | "enablePullDownRefresh": false |
| | | } |
| New file |
| | |
| | | <view class="container"> |
| | | <view wx:if="{{loading}}" class="loading">加载中...</view> |
| | | <view wx:else> |
| | | <view class="info-card"> |
| | | <text class="section-title">项目信息</text> |
| | | <view class="info-row"> |
| | | <text class="label">比赛名称</text> |
| | | <text class="value">{{detail.activityName || '-'}} </text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">项目名称</text> |
| | | <text class="value">{{detail.projectName || '-'}} </text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">当前状态</text> |
| | | <text class="status">{{detail.stateText || '未知'}}</text> |
| | | </view> |
| | | <view class="info-block"> |
| | | <text class="label">项目简介</text> |
| | | <text class="description">{{detail.description || '暂无简介'}}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="info-card"> |
| | | <text class="section-title">学员信息</text> |
| | | <view class="info-row"> |
| | | <text class="label">姓名</text> |
| | | <text class="value">{{detail.playerInfo.name || '-'}}</text> |
| | | </view> |
| | | <view class="info-row"> |
| | | <text class="label">联系方式</text> |
| | | <text class="value">{{detail.playerInfo.phone || '-'}}</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="info-card" wx:if="{{detail.submissionFiles && detail.submissionFiles.length}}"> |
| | | <text class="section-title">提交资料</text> |
| | | <view class="file-item" wx:for="{{detail.submissionFiles}}" wx:key="id" data-url="{{item.url}}" bindtap="previewFile"> |
| | | <text class="file-name">{{item.name || '资料文件'}}</text> |
| | | <text class="file-action">预览</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="info-card"> |
| | | <text class="section-title">审核意见</text> |
| | | <textarea |
| | | class="feedback-input" |
| | | placeholder="请输入审核意见(可选)" |
| | | value="{{feedback}}" |
| | | bindinput="onFeedbackInput" |
| | | maxlength="500" |
| | | auto-height |
| | | /> |
| | | </view> |
| | | |
| | | <view class="action-bar"> |
| | | <button |
| | | class="reject-btn" |
| | | bindtap="onReject" |
| | | loading="{{submitting}}" |
| | | disabled="{{submitting}}" |
| | | >驳回</button> |
| | | <button |
| | | class="approve-btn" |
| | | bindtap="onApprove" |
| | | loading="{{submitting}}" |
| | | disabled="{{submitting}}" |
| | | >审核通过</button> |
| | | </view> |
| | | </view> |
| | | </view> |
| New file |
| | |
| | | .container { |
| | | min-height: 100vh; |
| | | background: #f5f5f5; |
| | | padding: 24rpx; |
| | | } |
| | | |
| | | .loading { |
| | | text-align: center; |
| | | color: #666666; |
| | | margin-top: 200rpx; |
| | | font-size: 28rpx; |
| | | } |
| | | |
| | | .info-card { |
| | | background: #ffffff; |
| | | border-radius: 16rpx; |
| | | padding: 24rpx; |
| | | margin-bottom: 24rpx; |
| | | box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 32rpx; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | margin-bottom: 16rpx; |
| | | display: block; |
| | | } |
| | | |
| | | .info-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | font-size: 28rpx; |
| | | color: #444444; |
| | | margin-bottom: 12rpx; |
| | | } |
| | | |
| | | .label { |
| | | color: #888888; |
| | | } |
| | | |
| | | .value { |
| | | color: #333333; |
| | | } |
| | | |
| | | .status { |
| | | color: #1677ff; |
| | | } |
| | | |
| | | .info-block { |
| | | margin-top: 12rpx; |
| | | } |
| | | |
| | | .description { |
| | | color: #555555; |
| | | font-size: 26rpx; |
| | | line-height: 1.6; |
| | | margin-top: 8rpx; |
| | | } |
| | | |
| | | .file-item { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 20rpx 0; |
| | | border-bottom: 1rpx solid #f0f0f0; |
| | | font-size: 26rpx; |
| | | } |
| | | |
| | | .file-item:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .file-name { |
| | | color: #333333; |
| | | flex: 1; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | padding-right: 20rpx; |
| | | } |
| | | |
| | | .file-action { |
| | | color: #1677ff; |
| | | } |
| | | |
| | | .feedback-input { |
| | | background: #f8f8f8; |
| | | border-radius: 12rpx; |
| | | padding: 16rpx; |
| | | min-height: 120rpx; |
| | | font-size: 26rpx; |
| | | color: #333333; |
| | | } |
| | | |
| | | .action-bar { |
| | | position: sticky; |
| | | bottom: 0; |
| | | display: flex; |
| | | gap: 16rpx; |
| | | background: #f5f5f5; |
| | | padding-bottom: 24rpx; |
| | | } |
| | | |
| | | .reject-btn { |
| | | flex: 1; |
| | | background: #ffffff; |
| | | color: #ff5454; |
| | | border: 2rpx solid #ff5454; |
| | | border-radius: 36rpx; |
| | | height: 80rpx; |
| | | line-height: 80rpx; |
| | | font-size: 28rpx; |
| | | } |
| | | |
| | | .approve-btn { |
| | | flex: 1; |
| | | background: linear-gradient(135deg, #16a75c 0%, #4bc077 100%); |
| | | color: #ffffff; |
| | | border-radius: 36rpx; |
| | | height: 80rpx; |
| | | line-height: 80rpx; |
| | | font-size: 28rpx; |
| | | } |
| New file |
| | |
| | | const { graphqlRequest } = require('../../lib/utils') |
| | | |
| | | const LIST_QUERY = ` |
| | | query EmployeeReviewApplications($keyword: String, $state: Int, $page: Int, $size: Int) { |
| | | employeeReviewApplications(keyword: $keyword, state: $state, page: $page, size: $size) { |
| | | content { |
| | | id |
| | | playerName |
| | | projectName |
| | | activityName |
| | | state |
| | | stateText |
| | | stateType |
| | | applyTime |
| | | } |
| | | totalElements |
| | | page |
| | | size |
| | | } |
| | | } |
| | | ` |
| | | |
| | | const STATS_QUERY = ` |
| | | query EmployeeReviewStats($keyword: String) { |
| | | employeeReviewStats(keyword: $keyword) { |
| | | pendingCount |
| | | approvedCount |
| | | rejectedCount |
| | | } |
| | | } |
| | | ` |
| | | |
| | | Page({ |
| | | data: { |
| | | loading: false, |
| | | loadingMore: false, |
| | | hasMore: true, |
| | | tabs: [ |
| | | { label: '待审核', value: 0 }, |
| | | { label: '已审核', value: 1 }, |
| | | { label: '驳回', value: 2 } |
| | | ], |
| | | currentTab: 0, |
| | | searchKeyword: '', |
| | | list: [], |
| | | page: 1, |
| | | pageSize: 10, |
| | | stats: { |
| | | pendingCount: 0, |
| | | approvedCount: 0, |
| | | rejectedCount: 0 |
| | | }, |
| | | employeeReviewStats: { |
| | | pendingCount: 0, |
| | | approvedCount: 0, |
| | | rejectedCount: 0 |
| | | }, |
| | | needRefresh: false |
| | | }, |
| | | |
| | | onLoad() { |
| | | this.initData() |
| | | }, |
| | | |
| | | onShow() { |
| | | if (this.data.needRefresh) { |
| | | this.initData() |
| | | this.setData({ needRefresh: false }) |
| | | } |
| | | }, |
| | | |
| | | onPullDownRefresh() { |
| | | this.initData().finally(() => { |
| | | wx.stopPullDownRefresh() |
| | | }) |
| | | }, |
| | | |
| | | onReachBottom() { |
| | | if (this.data.hasMore && !this.data.loadingMore) { |
| | | this.loadList(false) |
| | | } |
| | | }, |
| | | |
| | | async initData() { |
| | | this.setData({ loading: true, page: 1, list: [], hasMore: true }) |
| | | |
| | | try { |
| | | await Promise.all([ |
| | | this.loadList(true), |
| | | this.loadStats(this.data.searchKeyword) |
| | | ]) |
| | | } catch (error) { |
| | | console.error('初始化审核数据失败:', error) |
| | | } finally { |
| | | this.setData({ loading: false }) |
| | | } |
| | | }, |
| | | |
| | | async loadStats(keyword) { |
| | | try { |
| | | const variables = {} |
| | | const trimmed = keyword && typeof keyword === 'string' ? keyword.trim() : '' |
| | | if (trimmed) { |
| | | variables.keyword = trimmed |
| | | } |
| | | |
| | | const result = await graphqlRequest(STATS_QUERY, variables) |
| | | if (result && result.employeeReviewStats) { |
| | | this.setData({ stats: result.employeeReviewStats, employeeReviewStats: result.employeeReviewStats }) |
| | | } |
| | | } catch (error) { |
| | | console.error('加载审核统计失败:', error) |
| | | } |
| | | }, |
| | | |
| | | async loadList(reset = false) { |
| | | if (reset) { |
| | | this.setData({ loading: true }) |
| | | } else { |
| | | this.setData({ loadingMore: true }) |
| | | } |
| | | |
| | | const nextPage = reset ? 1 : this.data.page + 1 |
| | | const keywordInput = this.data.searchKeyword || '' |
| | | const trimmedKeyword = keywordInput.trim() |
| | | const variables = { |
| | | keyword: trimmedKeyword ? trimmedKeyword : null, |
| | | state: this.getStateByTab(this.data.currentTab), |
| | | page: nextPage, |
| | | size: this.data.pageSize |
| | | } |
| | | |
| | | try { |
| | | const result = await graphqlRequest(LIST_QUERY, variables) |
| | | const pageData = result && result.employeeReviewApplications |
| | | const items = pageData && Array.isArray(pageData.content) ? pageData.content : [] |
| | | const list = reset ? items : this.data.list.concat(items) |
| | | const total = pageData && typeof pageData.totalElements === 'number' ? pageData.totalElements : 0 |
| | | const hasMore = nextPage * this.data.pageSize < total |
| | | |
| | | this.setData({ |
| | | list, |
| | | page: nextPage, |
| | | hasMore |
| | | }) |
| | | } catch (error) { |
| | | console.error('加载审核列表失败:', error) |
| | | wx.showToast({ title: '加载失败', icon: 'none' }) |
| | | } finally { |
| | | if (reset) { |
| | | this.setData({ loading: false }) |
| | | } else { |
| | | this.setData({ loadingMore: false }) |
| | | } |
| | | } |
| | | }, |
| | | |
| | | onSearchInput(e) { |
| | | this.setData({ searchKeyword: e.detail.value || '' }) |
| | | }, |
| | | |
| | | onSearch() { |
| | | this.initData() |
| | | }, |
| | | |
| | | clearSearch() { |
| | | if (!this.data.searchKeyword) return |
| | | this.setData({ searchKeyword: '' }) |
| | | this.initData() |
| | | }, |
| | | |
| | | switchTab(e) { |
| | | const index = Number(e.currentTarget.dataset.index || 0) |
| | | if (index === this.data.currentTab) { |
| | | return |
| | | } |
| | | |
| | | this.setData({ |
| | | currentTab: index, |
| | | page: 1, |
| | | list: [], |
| | | hasMore: true |
| | | }) |
| | | |
| | | this.initData() |
| | | }, |
| | | |
| | | getStateByTab(index) { |
| | | switch (index) { |
| | | case 0: |
| | | return 0 |
| | | case 1: |
| | | return 1 |
| | | case 2: |
| | | return 2 |
| | | default: |
| | | return null |
| | | } |
| | | }, |
| | | |
| | | goToDetail(e) { |
| | | const id = e.currentTarget.dataset.id |
| | | if (!id) { |
| | | wx.showToast({ title: '报名ID无效', icon: 'none' }) |
| | | return |
| | | } |
| | | |
| | | wx.navigateTo({ |
| | | url: `/pages/profile/employee-review-detail?id=${id}`, |
| | | events: { |
| | | auditUpdated: () => { |
| | | this.setData({ needRefresh: true }) |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | }) |
| New file |
| | |
| | | { |
| | | "navigationBarTitleText": "我的审核", |
| | | "enablePullDownRefresh": true, |
| | | "backgroundTextStyle": "dark" |
| | | } |
| New file |
| | |
| | | <view class="container"> |
| | | <view class="search-bar"> |
| | | <input |
| | | class="search-input" |
| | | value="{{searchKeyword}}" |
| | | placeholder="搜索比赛 / 项目 / 学员" |
| | | bindinput="onSearchInput" |
| | | confirm-type="search" |
| | | bindconfirm="onSearch" |
| | | /> |
| | | <view class="search-actions"> |
| | | <button class="primary-btn" bindtap="onSearch">搜索</button> |
| | | <button class="plain-btn" bindtap="clearSearch" wx:if="{{searchKeyword}}">清空</button> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="tab-bar"> |
| | | <view |
| | | class="tab-item {{currentTab === index ? 'active' : ''}}" |
| | | wx:for="{{tabs}}" |
| | | wx:key="value" |
| | | data-index="{{index}}" |
| | | bindtap="switchTab" |
| | | > |
| | | <text class="tab-label">{{item.label}}</text> |
| | | <text class="tab-count"> |
| | | {{item.value === 0 ? employeeReviewStats.pendingCount || 0 : |
| | | item.value === 1 ? employeeReviewStats.approvedCount || 0 : |
| | | employeeReviewStats.rejectedCount || 0}} |
| | | </text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="list-container"> |
| | | <view wx:if="{{list.length > 0}}"> |
| | | <view class="audit-card" wx:for="{{list}}" wx:key="id"> |
| | | <view class="card-header"> |
| | | <text class="card-title">{{item.projectName || '未命名项目'}}</text> |
| | | <text class="card-status status-{{item.stateType || 'info'}}">{{item.stateText || ''}}</text> |
| | | </view> |
| | | <view class="card-row"> |
| | | <text class="card-label">比赛</text> |
| | | <text class="card-value">{{item.activityName || '-'}}</text> |
| | | </view> |
| | | <view class="card-row"> |
| | | <text class="card-label">学员</text> |
| | | <text class="card-value">{{item.playerName || '-'}}</text> |
| | | </view> |
| | | <view class="card-row"> |
| | | <text class="card-label">报名时间</text> |
| | | <text class="card-value">{{item.applyTime || '-'}}</text> |
| | | </view> |
| | | <view class="card-footer"> |
| | | <text class="card-tip">最新状态:{{item.stateText || '未知'}}</text> |
| | | <button class="action-btn" data-id="{{item.id}}" bindtap="goToDetail">审核</button> |
| | | </view> |
| | | </view> |
| | | <view class="load-more" wx:if="{{loadingMore}}"> |
| | | <text>加载中...</text> |
| | | </view> |
| | | <view class="load-more" wx:elif="{{!hasMore}}"> |
| | | <text>没有更多数据</text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view wx:else class="empty-state"> |
| | | <text class="empty-icon">📂</text> |
| | | <text class="empty-text">暂无符合条件的项目</text> |
| | | <text class="empty-desc">尝试调整筛选条件或搜索其他关键词</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| New file |
| | |
| | | .container { |
| | | min-height: 100vh; |
| | | background: #f5f5f5; |
| | | padding: 24rpx; |
| | | } |
| | | |
| | | .search-bar { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 16rpx; |
| | | margin-bottom: 24rpx; |
| | | } |
| | | |
| | | .search-input { |
| | | flex: 1; |
| | | height: 72rpx; |
| | | background: #ffffff; |
| | | border-radius: 36rpx; |
| | | padding: 0 24rpx; |
| | | font-size: 28rpx; |
| | | box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .search-actions { |
| | | display: flex; |
| | | gap: 12rpx; |
| | | } |
| | | |
| | | .primary-btn { |
| | | background: #1677ff; |
| | | color: #ffffff; |
| | | border-radius: 36rpx; |
| | | padding: 0 32rpx; |
| | | height: 72rpx; |
| | | line-height: 72rpx; |
| | | font-size: 28rpx; |
| | | } |
| | | |
| | | .plain-btn { |
| | | background: #ffffff; |
| | | color: #1677ff; |
| | | border-radius: 36rpx; |
| | | padding: 0 32rpx; |
| | | height: 72rpx; |
| | | line-height: 72rpx; |
| | | font-size: 28rpx; |
| | | border: 2rpx solid #1677ff; |
| | | } |
| | | |
| | | .tab-bar { |
| | | display: flex; |
| | | background: #ffffff; |
| | | border-radius: 16rpx; |
| | | padding: 12rpx; |
| | | margin-bottom: 24rpx; |
| | | box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .tab-item { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | padding: 20rpx 0; |
| | | border-radius: 12rpx; |
| | | font-size: 26rpx; |
| | | color: #666666; |
| | | transition: all 0.2s ease; |
| | | } |
| | | |
| | | .tab-item.active { |
| | | background: linear-gradient(135deg, #1677ff 0%, #4091ff 100%); |
| | | color: #ffffff; |
| | | } |
| | | |
| | | .tab-count { |
| | | margin-top: 8rpx; |
| | | font-size: 32rpx; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .list-container { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20rpx; |
| | | } |
| | | |
| | | .audit-card { |
| | | background: #ffffff; |
| | | border-radius: 16rpx; |
| | | padding: 24rpx; |
| | | box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16rpx; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .card-title { |
| | | font-size: 32rpx; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | .card-status { |
| | | font-size: 26rpx; |
| | | padding: 4rpx 16rpx; |
| | | border-radius: 24rpx; |
| | | } |
| | | |
| | | .status-warning { |
| | | background: rgba(255, 180, 0, 0.15); |
| | | color: #ffa114; |
| | | } |
| | | |
| | | .status-success { |
| | | background: rgba(17, 201, 128, 0.15); |
| | | color: #16a75c; |
| | | } |
| | | |
| | | .status-danger { |
| | | background: rgba(255, 84, 84, 0.15); |
| | | color: #ff5454; |
| | | } |
| | | |
| | | .status-info { |
| | | background: rgba(64, 145, 255, 0.15); |
| | | color: #1677ff; |
| | | } |
| | | |
| | | .card-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | font-size: 26rpx; |
| | | color: #444444; |
| | | } |
| | | |
| | | .card-label { |
| | | color: #888888; |
| | | } |
| | | |
| | | .card-footer { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-top: 8rpx; |
| | | } |
| | | |
| | | .card-tip { |
| | | font-size: 24rpx; |
| | | color: #999999; |
| | | } |
| | | |
| | | .action-btn { |
| | | background: linear-gradient(135deg, #1677ff 0%, #4091ff 100%); |
| | | color: #ffffff; |
| | | padding: 0 32rpx; |
| | | height: 64rpx; |
| | | line-height: 64rpx; |
| | | border-radius: 32rpx; |
| | | font-size: 26rpx; |
| | | } |
| | | |
| | | .load-more { |
| | | text-align: center; |
| | | color: #999999; |
| | | font-size: 26rpx; |
| | | padding: 20rpx 0; |
| | | } |
| | | |
| | | .empty-state { |
| | | margin-top: 120rpx; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | color: #999999; |
| | | gap: 12rpx; |
| | | } |
| | | |
| | | .empty-icon { |
| | | font-size: 72rpx; |
| | | } |
| | | |
| | | .empty-text { |
| | | font-size: 28rpx; |
| | | } |
| | | |
| | | .empty-desc { |
| | | font-size: 24rpx; |
| | | } |
| | |
| | | isJudge: false, |
| | | isOrganizer: false, |
| | | hasPlayer: false, |
| | | isEmployee: false, |
| | | |
| | | // 员工审核统计 |
| | | employeeReviewStats: { |
| | | pendingCount: 0, |
| | | approvedCount: 0, |
| | | rejectedCount: 0 |
| | | }, |
| | | |
| | | // 评委相关数据 |
| | | judgeStats: { |
| | |
| | | grade |
| | | roles |
| | | createdAt |
| | | employee { |
| | | id |
| | | name |
| | | roleId |
| | | description |
| | | } |
| | | player { |
| | | id |
| | | name |
| | |
| | | const isJudge = userRoles.includes('JUDGE') |
| | | const isOrganizer = userRoles.includes('ORGANIZER') |
| | | const hasPlayer = userInfo.player && userInfo.player.id |
| | | const isEmployee = !!(userInfo.employee && userInfo.employee.id) |
| | | |
| | | // 处理头像文字 |
| | | const avatarText = (userInfo.name || '用户').substring(0, 1) |
| | |
| | | userRoles, |
| | | isJudge, |
| | | isOrganizer, |
| | | hasPlayer |
| | | hasPlayer, |
| | | isEmployee |
| | | }) |
| | | |
| | | // 更新全局用户信息 |
| | |
| | | |
| | | if (isOrganizer) { |
| | | this.loadOrganizerStats() |
| | | } |
| | | |
| | | if (isEmployee) { |
| | | this.loadEmployeeReviewStats() |
| | | } else { |
| | | this.setData({ |
| | | employeeReviewStats: { |
| | | pendingCount: 0, |
| | | approvedCount: 0, |
| | | rejectedCount: 0 |
| | | } |
| | | }) |
| | | } |
| | | } |
| | | } catch (error) { |
| | |
| | | } |
| | | }, |
| | | |
| | | // 加载员工审核统计 |
| | | async loadEmployeeReviewStats(keyword = null) { |
| | | if (!this.data.isEmployee) { |
| | | return |
| | | } |
| | | |
| | | try { |
| | | const query = ` |
| | | query EmployeeReviewStats($keyword: String) { |
| | | employeeReviewStats(keyword: $keyword) { |
| | | pendingCount |
| | | approvedCount |
| | | rejectedCount |
| | | } |
| | | } |
| | | ` |
| | | |
| | | const variables = {} |
| | | if (keyword && typeof keyword === 'string' && keyword.trim()) { |
| | | variables.keyword = keyword.trim() |
| | | } |
| | | |
| | | const result = await graphqlRequest(query, variables) |
| | | |
| | | if (result && result.employeeReviewStats) { |
| | | this.setData({ |
| | | employeeReviewStats: result.employeeReviewStats |
| | | }) |
| | | } |
| | | } catch (error) { |
| | | console.error('加载员工审核统计失败:', error) |
| | | } |
| | | }, |
| | | // 加载评委统计数据 |
| | | async loadJudgeStats() { |
| | | try { |
| | |
| | | }) |
| | | }, |
| | | |
| | | // 跳转到员工审核页面 |
| | | goToEmployeeReviewPage() { |
| | | if (!this.data.isEmployee) { |
| | | return |
| | | } |
| | | |
| | | wx.navigateTo({ |
| | | url: '/pages/profile/employee-review' |
| | | }) |
| | | }, |
| | | |
| | | |
| | | |
| | | // 查看报名详情 |
| | |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 我的审核区域 - 仅员工可见 --> |
| | | <view class="review-section" wx:if="{{isEmployee}}"> |
| | | <view class="section-header"> |
| | | <text class="section-title">我的审核</text> |
| | | <view class="review-action-btn" bindtap="goToEmployeeReviewPage"> |
| | | <text class="action-text">审核</text> |
| | | <text class="action-arrow">></text> |
| | | </view> |
| | | </view> |
| | | |
| | | <view class="review-stats"> |
| | | <view class="stat-item"> |
| | | <text class="stat-number">{{employeeReviewStats.pendingCount || 0}}</text> |
| | | <text class="stat-label">待审核</text> |
| | | </view> |
| | | <view class="stat-divider"></view> |
| | | <view class="stat-item"> |
| | | <text class="stat-number">{{employeeReviewStats.approvedCount || 0}}</text> |
| | | <text class="stat-label">已审核</text> |
| | | </view> |
| | | <view class="stat-divider"></view> |
| | | <view class="stat-item"> |
| | | <text class="stat-number">{{employeeReviewStats.rejectedCount || 0}}</text> |
| | | <text class="stat-label">驳回</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | </view> |
| | | </view> |