audit_list.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import { Image } from "expo-image";
  2. import { useLocalSearchParams, useRouter } from "expo-router";
  3. import React, { useCallback, useEffect, useState } from "react";
  4. import {
  5. ActivityIndicator,
  6. Alert,
  7. FlatList,
  8. ImageBackground,
  9. RefreshControl,
  10. StatusBar,
  11. StyleSheet,
  12. Text,
  13. TouchableOpacity,
  14. View,
  15. } from "react-native";
  16. import { useSafeAreaInsets } from "react-native-safe-area-context";
  17. import { Images } from "@/constants/images";
  18. import {
  19. roomAuditPass,
  20. roomAuditRecord,
  21. roomAuditUnpass,
  22. } from "@/services/dimension";
  23. interface AuditItem {
  24. id: string;
  25. avatar: string;
  26. nickname: string;
  27. auditStatus: number; // 0-待审核 1-拒绝 2-通过
  28. }
  29. const TABS = [
  30. { title: "待审核", value: 0 },
  31. { title: "审核通过", value: 2 },
  32. { title: "审核不通过", value: 1 },
  33. ];
  34. export default function AuditListScreen() {
  35. const { id: roomId } = useLocalSearchParams<{ id: string }>();
  36. const router = useRouter();
  37. const insets = useSafeAreaInsets();
  38. const [activeTab, setActiveTab] = useState(0);
  39. const [list, setList] = useState<AuditItem[]>([]);
  40. const [loading, setLoading] = useState(false);
  41. const [refreshing, setRefreshing] = useState(false);
  42. const [page, setPage] = useState(1);
  43. const [hasMore, setHasMore] = useState(true);
  44. const loadData = useCallback(
  45. async (pageNum: number, isRefresh = false) => {
  46. if (loading && !isRefresh) return;
  47. try {
  48. if (pageNum === 1) setLoading(true);
  49. const data = await roomAuditRecord(
  50. pageNum,
  51. 20,
  52. roomId as string,
  53. TABS[activeTab].value,
  54. );
  55. const records = Array.isArray(data)
  56. ? data
  57. : data?.records || [];
  58. if (records.length < 20) setHasMore(false);
  59. if (pageNum === 1 || isRefresh) {
  60. setList(records);
  61. } else {
  62. setList((prev) => [...prev, ...records]);
  63. }
  64. } catch (e) {
  65. console.error("加载审核记录失败:", e);
  66. } finally {
  67. setLoading(false);
  68. setRefreshing(false);
  69. }
  70. },
  71. [roomId, activeTab, loading],
  72. );
  73. useEffect(() => {
  74. setPage(1);
  75. setList([]);
  76. setHasMore(true);
  77. loadData(1, true);
  78. }, [activeTab]);
  79. const handleRefresh = () => {
  80. setRefreshing(true);
  81. setPage(1);
  82. setHasMore(true);
  83. loadData(1, true);
  84. };
  85. const handleLoadMore = () => {
  86. if (!loading && hasMore) {
  87. const np = page + 1;
  88. setPage(np);
  89. loadData(np);
  90. }
  91. };
  92. const handlePass = async (item: AuditItem, index: number) => {
  93. try {
  94. const res = await roomAuditPass(item.id);
  95. if (res) {
  96. const newList = [...list];
  97. newList[index] = { ...item, auditStatus: 2 };
  98. setList(newList);
  99. }
  100. } catch (e) {
  101. Alert.alert("提示", "操作失败");
  102. }
  103. };
  104. const handleReject = async (item: AuditItem, index: number) => {
  105. try {
  106. const res = await roomAuditUnpass(item.id);
  107. if (res) {
  108. const newList = [...list];
  109. newList[index] = { ...item, auditStatus: 1 };
  110. setList(newList);
  111. }
  112. } catch (e) {
  113. Alert.alert("提示", "操作失败");
  114. }
  115. };
  116. const renderItem = ({
  117. item,
  118. index,
  119. }: {
  120. item: AuditItem;
  121. index: number;
  122. }) => (
  123. <View style={styles.cell}>
  124. <Image
  125. source={{ uri: item.avatar }}
  126. style={styles.avatar}
  127. contentFit="cover"
  128. />
  129. <Text style={styles.nickname} numberOfLines={1}>
  130. {item.nickname}
  131. </Text>
  132. <View style={styles.actionArea}>
  133. {item.auditStatus === 0 ? (
  134. <>
  135. <TouchableOpacity
  136. style={[styles.btn, styles.rejectBtn]}
  137. onPress={() => handleReject(item, index)}
  138. >
  139. <Text style={styles.btnText}>拒绝</Text>
  140. </TouchableOpacity>
  141. <TouchableOpacity
  142. style={[styles.btn, styles.passBtn]}
  143. onPress={() => handlePass(item, index)}
  144. >
  145. <Text style={styles.btnText}>通过</Text>
  146. </TouchableOpacity>
  147. </>
  148. ) : (
  149. <Text style={styles.statusText}>
  150. {item.auditStatus === 2 ? "通过" : "拒绝"}
  151. </Text>
  152. )}
  153. </View>
  154. </View>
  155. );
  156. return (
  157. <View style={styles.container}>
  158. <StatusBar barStyle="light-content" />
  159. <ImageBackground
  160. source={{ uri: Images.mine.kaixinMineBg }}
  161. style={styles.background}
  162. resizeMode="cover"
  163. >
  164. {/* 顶部导航 */}
  165. <View style={[styles.header, { paddingTop: insets.top }]}>
  166. <TouchableOpacity
  167. style={styles.backBtn}
  168. onPress={() => router.back()}
  169. >
  170. <Text style={styles.backText}>←</Text>
  171. </TouchableOpacity>
  172. <Text style={styles.title}>审核</Text>
  173. <View style={styles.placeholder} />
  174. </View>
  175. {/* Tab 切换 */}
  176. <View style={styles.tabs}>
  177. {TABS.map((tab, index) => {
  178. const isActive = activeTab === index;
  179. return (
  180. <TouchableOpacity
  181. key={index}
  182. style={[styles.tabItem, isActive && styles.tabItemActive]}
  183. onPress={() => setActiveTab(index)}
  184. >
  185. <Text
  186. style={[
  187. styles.tabText,
  188. isActive && styles.tabTextActive,
  189. ]}
  190. >
  191. {tab.title}
  192. </Text>
  193. {isActive && <View style={styles.tabLine} />}
  194. </TouchableOpacity>
  195. );
  196. })}
  197. </View>
  198. {/* 列表 */}
  199. <FlatList
  200. data={list}
  201. renderItem={renderItem}
  202. keyExtractor={(item, index) => item.id || index.toString()}
  203. contentContainerStyle={styles.listContent}
  204. refreshControl={
  205. <RefreshControl
  206. refreshing={refreshing}
  207. onRefresh={handleRefresh}
  208. tintColor="#fff"
  209. />
  210. }
  211. onEndReached={handleLoadMore}
  212. onEndReachedThreshold={0.3}
  213. ListFooterComponent={
  214. loading && list.length > 0 ? (
  215. <ActivityIndicator
  216. color="#fff"
  217. style={{ marginVertical: 10 }}
  218. />
  219. ) : null
  220. }
  221. ListEmptyComponent={
  222. !loading ? (
  223. <View style={styles.emptyBox}>
  224. <Text style={styles.emptyText}>暂无记录</Text>
  225. </View>
  226. ) : null
  227. }
  228. />
  229. </ImageBackground>
  230. </View>
  231. );
  232. }
  233. const styles = StyleSheet.create({
  234. container: { flex: 1, backgroundColor: "#1a1a2e" },
  235. background: { flex: 1 },
  236. header: {
  237. flexDirection: "row",
  238. alignItems: "center",
  239. justifyContent: "space-between",
  240. paddingHorizontal: 15,
  241. paddingBottom: 10,
  242. },
  243. backBtn: {
  244. width: 40,
  245. height: 40,
  246. justifyContent: "center",
  247. alignItems: "center",
  248. },
  249. backText: { color: "#fff", fontSize: 20 },
  250. title: { color: "#fff", fontSize: 16, fontWeight: "bold" },
  251. placeholder: { width: 40 },
  252. // Tabs
  253. tabs: {
  254. flexDirection: "row",
  255. paddingHorizontal: 10,
  256. borderBottomWidth: 1,
  257. borderBottomColor: "rgba(255,255,255,0.15)",
  258. },
  259. tabItem: {
  260. flex: 1,
  261. height: 44,
  262. justifyContent: "center",
  263. alignItems: "center",
  264. position: "relative",
  265. },
  266. tabItemActive: {},
  267. tabText: { color: "#999", fontSize: 14 },
  268. tabTextActive: { color: "#fff", fontWeight: "bold" },
  269. tabLine: {
  270. position: "absolute",
  271. bottom: 0,
  272. width: "60%",
  273. height: 2,
  274. backgroundColor: "#e79018",
  275. },
  276. // List
  277. listContent: { padding: 15, paddingBottom: 100 },
  278. cell: {
  279. flexDirection: "row",
  280. alignItems: "center",
  281. backgroundColor: "rgba(255,255,255,0.9)",
  282. borderRadius: 8,
  283. paddingHorizontal: 15,
  284. paddingVertical: 12,
  285. marginBottom: 10,
  286. },
  287. avatar: {
  288. width: 40,
  289. height: 40,
  290. borderRadius: 4,
  291. borderWidth: 2,
  292. borderColor: "#000",
  293. marginRight: 12,
  294. },
  295. nickname: {
  296. flex: 1,
  297. fontSize: 14,
  298. color: "#333",
  299. fontWeight: "500",
  300. },
  301. actionArea: {
  302. flexDirection: "row",
  303. alignItems: "center",
  304. },
  305. btn: {
  306. width: 68,
  307. height: 32,
  308. borderRadius: 4,
  309. justifyContent: "center",
  310. alignItems: "center",
  311. marginLeft: 8,
  312. },
  313. rejectBtn: { backgroundColor: "#ff4d4f" },
  314. passBtn: { backgroundColor: "#52c41a" },
  315. btnText: { color: "#fff", fontSize: 13, fontWeight: "bold" },
  316. statusText: {
  317. fontSize: 13,
  318. color: "#666",
  319. fontWeight: "500",
  320. },
  321. emptyBox: { alignItems: "center", paddingTop: 80 },
  322. emptyText: { color: "#999", fontSize: 14 },
  323. });