From 915d80766dd8e0157e9b9510b3634ed758eb5c5a Mon Sep 17 00:00:00 2001
From: Codex Assistant <codex@example.com>
Date: 星期日, 05 十月 2025 14:45:58 +0800
Subject: [PATCH] feat: 新增员工审核入口与审核页面
---
backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewStatsResponse.java | 43 +
backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewPageResponse.java | 55 ++
wx/pages/profile/employee-review.json | 5
wx/pages/profile/employee-review.js | 217 ++++++++
wx/pages/profile/employee-review-detail.wxss | 124 ++++
wx/pages/profile/employee-review-detail.wxml | 71 ++
backend/src/main/java/com/rongyichuang/employee/resolver/EmployeeReviewResolver.java | 34 +
wx/pages/profile/profile.wxml | 29 +
wx/pages/profile/employee-review-detail.json | 4
backend/src/main/java/com/rongyichuang/employee/service/EmployeeReviewService.java | 248 +++++++++
wx/pages/profile/profile.js | 74 ++
wx/app.json | 50 +
backend/src/main/resources/graphql/employee.graphqls | 32 +
wx/pages/profile/employee-review.wxml | 72 ++
wx/pages/profile/employee-review.wxss | 195 +++++++
backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewApplicationResponse.java | 82 +++
wx/pages/profile/employee-review-detail.js | 165 ++++++
17 files changed, 1,474 insertions(+), 26 deletions(-)
diff --git a/backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewApplicationResponse.java b/backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewApplicationResponse.java
new file mode 100644
index 0000000..01391fa
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewApplicationResponse.java
@@ -0,0 +1,82 @@
+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;
+ }
+}
diff --git a/backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewPageResponse.java b/backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewPageResponse.java
new file mode 100644
index 0000000..a8d770e
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewPageResponse.java
@@ -0,0 +1,55 @@
+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;
+ }
+}
diff --git a/backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewStatsResponse.java b/backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewStatsResponse.java
new file mode 100644
index 0000000..117f8e9
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/employee/dto/response/EmployeeReviewStatsResponse.java
@@ -0,0 +1,43 @@
+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;
+ }
+}
diff --git a/backend/src/main/java/com/rongyichuang/employee/resolver/EmployeeReviewResolver.java b/backend/src/main/java/com/rongyichuang/employee/resolver/EmployeeReviewResolver.java
new file mode 100644
index 0000000..d0c1e4a
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/employee/resolver/EmployeeReviewResolver.java
@@ -0,0 +1,34 @@
+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);
+ }
+}
diff --git a/backend/src/main/java/com/rongyichuang/employee/service/EmployeeReviewService.java b/backend/src/main/java/com/rongyichuang/employee/service/EmployeeReviewService.java
new file mode 100644
index 0000000..e0a32e8
--- /dev/null
+++ b/backend/src/main/java/com/rongyichuang/employee/service/EmployeeReviewService.java
@@ -0,0 +1,248 @@
+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("鏃犳硶瑙f瀽鏁村瀷鍊�: {}", 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("鏃犳硶瑙f瀽闀挎暣鍨嬪��: {}", 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";
+ };
+ }
+}
diff --git a/backend/src/main/resources/graphql/employee.graphqls b/backend/src/main/resources/graphql/employee.graphqls
index 78727ab..621d614 100644
--- a/backend/src/main/resources/graphql/employee.graphqls
+++ b/backend/src/main/resources/graphql/employee.graphqls
@@ -26,6 +26,32 @@
# Spring Data鐨凱age瀵硅薄浼氳嚜鍔ㄦ槧灏勫埌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!]!
@@ -35,6 +61,12 @@
# 鏍规嵁ID鑾峰彇鍛樺伐璇︽儏
employee(id: Long!): EmployeeResponse
+
+ # 鍛樺伐瀹℃牳缁熻
+ employeeReviewStats(keyword: String): EmployeeReviewStats!
+
+ # 鍛樺伐瀹℃牳鍒楄〃
+ employeeReviewApplications(keyword: String, state: Int, page: Int, size: Int): EmployeeReviewPage!
}
# 鎵╁睍鍙樻洿绫诲瀷
diff --git a/wx/app.json b/wx/app.json
index e5d2a32..8bea719 100644
--- a/wx/app.json
+++ b/wx/app.json
@@ -5,6 +5,8 @@
"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",
@@ -19,28 +21,26 @@
"navigationBarTextStyle": "black",
"backgroundColor": "#f5f5f5"
},
-
- "tabBar": {
- "custom": true,
- "color": "#999999",
- "selectedColor": "#007aff",
- "backgroundColor": "#ffffff",
- "list": [
- {
- "pagePath": "pages/index/index",
- "text": "棣栭〉"
- },
- {
- "pagePath": "pages/message/message",
- "text": "娑堟伅"
- },
- {
- "pagePath": "pages/profile/profile",
- "text": "鎴戠殑"
- }
- ]
- },
-
+ "tabBar": {
+ "custom": true,
+ "color": "#999999",
+ "selectedColor": "#007aff",
+ "backgroundColor": "#ffffff",
+ "list": [
+ {
+ "pagePath": "pages/index/index",
+ "text": "棣栭〉"
+ },
+ {
+ "pagePath": "pages/message/message",
+ "text": "娑堟伅"
+ },
+ {
+ "pagePath": "pages/profile/profile",
+ "text": "鎴戠殑"
+ }
+ ]
+ },
"networkTimeout": {
"request": 10000,
"downloadFile": 10000
@@ -51,6 +51,8 @@
"desc": "浣犵殑浣嶇疆淇℃伅灏嗙敤浜庡皬绋嬪簭浣嶇疆鎺ュ彛鐨勬晥鏋滃睍绀�"
}
},
- "requiredBackgroundModes": ["audio"],
+ "requiredBackgroundModes": [
+ "audio"
+ ],
"sitemapLocation": "sitemap.json"
-}
\ No newline at end of file
+}
diff --git a/wx/pages/profile/employee-review-detail.js b/wx/pages/profile/employee-review-detail.js
new file mode 100644
index 0000000..0b6dadc
--- /dev/null
+++ b/wx/pages/profile/employee-review-detail.js
@@ -0,0 +1,165 @@
+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 '鏈煡鐘舵��'
+ }
+ }
+})
diff --git a/wx/pages/profile/employee-review-detail.json b/wx/pages/profile/employee-review-detail.json
new file mode 100644
index 0000000..08545b4
--- /dev/null
+++ b/wx/pages/profile/employee-review-detail.json
@@ -0,0 +1,4 @@
+{
+ "navigationBarTitleText": "瀹℃牳璇︽儏",
+ "enablePullDownRefresh": false
+}
diff --git a/wx/pages/profile/employee-review-detail.wxml b/wx/pages/profile/employee-review-detail.wxml
new file mode 100644
index 0000000..b66c8a2
--- /dev/null
+++ b/wx/pages/profile/employee-review-detail.wxml
@@ -0,0 +1,71 @@
+<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>
diff --git a/wx/pages/profile/employee-review-detail.wxss b/wx/pages/profile/employee-review-detail.wxss
new file mode 100644
index 0000000..dc136a0
--- /dev/null
+++ b/wx/pages/profile/employee-review-detail.wxss
@@ -0,0 +1,124 @@
+.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;
+}
diff --git a/wx/pages/profile/employee-review.js b/wx/pages/profile/employee-review.js
new file mode 100644
index 0000000..5c3ad4a
--- /dev/null
+++ b/wx/pages/profile/employee-review.js
@@ -0,0 +1,217 @@
+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 })
+ }
+ }
+ })
+ }
+})
diff --git a/wx/pages/profile/employee-review.json b/wx/pages/profile/employee-review.json
new file mode 100644
index 0000000..271e618
--- /dev/null
+++ b/wx/pages/profile/employee-review.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "鎴戠殑瀹℃牳",
+ "enablePullDownRefresh": true,
+ "backgroundTextStyle": "dark"
+}
diff --git a/wx/pages/profile/employee-review.wxml b/wx/pages/profile/employee-review.wxml
new file mode 100644
index 0000000..a1c4e8c
--- /dev/null
+++ b/wx/pages/profile/employee-review.wxml
@@ -0,0 +1,72 @@
+<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>
diff --git a/wx/pages/profile/employee-review.wxss b/wx/pages/profile/employee-review.wxss
new file mode 100644
index 0000000..9979dbd
--- /dev/null
+++ b/wx/pages/profile/employee-review.wxss
@@ -0,0 +1,195 @@
+.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;
+}
diff --git a/wx/pages/profile/profile.js b/wx/pages/profile/profile.js
index 15cedc9..99de718 100644
--- a/wx/pages/profile/profile.js
+++ b/wx/pages/profile/profile.js
@@ -27,6 +27,14 @@
isJudge: false,
isOrganizer: false,
hasPlayer: false,
+ isEmployee: false,
+
+ // 鍛樺伐瀹℃牳缁熻
+ employeeReviewStats: {
+ pendingCount: 0,
+ approvedCount: 0,
+ rejectedCount: 0
+ },
// 璇勫鐩稿叧鏁版嵁
judgeStats: {
@@ -198,6 +206,12 @@
grade
roles
createdAt
+ employee {
+ id
+ name
+ roleId
+ description
+ }
player {
id
name
@@ -218,6 +232,7 @@
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)
@@ -228,7 +243,8 @@
userRoles,
isJudge,
isOrganizer,
- hasPlayer
+ hasPlayer,
+ isEmployee
})
// 鏇存柊鍏ㄥ眬鐢ㄦ埛淇℃伅
@@ -241,6 +257,18 @@
if (isOrganizer) {
this.loadOrganizerStats()
+ }
+
+ if (isEmployee) {
+ this.loadEmployeeReviewStats()
+ } else {
+ this.setData({
+ employeeReviewStats: {
+ pendingCount: 0,
+ approvedCount: 0,
+ rejectedCount: 0
+ }
+ })
}
}
} catch (error) {
@@ -358,6 +386,39 @@
}
},
+ // 鍔犺浇鍛樺伐瀹℃牳缁熻
+ 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 {
@@ -465,6 +526,17 @@
})
},
+ // 璺宠浆鍒板憳宸ュ鏍搁〉闈�
+ goToEmployeeReviewPage() {
+ if (!this.data.isEmployee) {
+ return
+ }
+
+ wx.navigateTo({
+ url: '/pages/profile/employee-review'
+ })
+ },
+
// 鏌ョ湅鎶ュ悕璇︽儏
diff --git a/wx/pages/profile/profile.wxml b/wx/pages/profile/profile.wxml
index fd4c5d4..6b3b802 100644
--- a/wx/pages/profile/profile.wxml
+++ b/wx/pages/profile/profile.wxml
@@ -97,6 +97,33 @@
</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>
\ No newline at end of file
+</view>
--
Gitblit v1.8.0