create.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. import { Ionicons } from '@expo/vector-icons';
  2. import { useRouter } from 'expo-router';
  3. import React, { useEffect, useState } from 'react';
  4. import {
  5. Alert,
  6. FlatList,
  7. Image,
  8. ImageBackground,
  9. Modal,
  10. ScrollView,
  11. StyleSheet,
  12. Switch,
  13. Text,
  14. TextInput,
  15. TouchableOpacity,
  16. View,
  17. } from 'react-native';
  18. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  19. import { Images } from '@/constants/images';
  20. import { createRoom } from '@/services/dimension';
  21. import event from '@/utils/event';
  22. const DateTimePickerModal = ({ visible, onClose, onConfirm }: any) => {
  23. const now = new Date();
  24. const [selDate, setSelDate] = useState(now.toISOString().split('T')[0]);
  25. const [selHour, setSelHour] = useState(String(now.getHours() + 1).padStart(2, '0'));
  26. const [selMin, setSelMin] = useState('00');
  27. const dates = Array.from({ length: 30 }, (_, i) => {
  28. const d = new Date();
  29. d.setDate(d.getDate() + i);
  30. return d.toISOString().split('T')[0];
  31. });
  32. // Handle wrap around for hours if needed, though simple 0-23 list is fine for selection
  33. const hours = Array.from({ length: 24 }, (_, i) => i.toString().padStart(2, '0'));
  34. const mins = ['00', '10', '20', '30', '40', '50'];
  35. useEffect(() => {
  36. if (visible) {
  37. if (!selDate) setSelDate(dates[0]);
  38. }
  39. }, [visible]);
  40. return (
  41. <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
  42. <TouchableOpacity style={styles.modalOverlay as any} activeOpacity={1} onPress={onClose}>
  43. <TouchableOpacity activeOpacity={1} style={styles.pickerContent as any}>
  44. <View style={styles.pickerHeader as any}>
  45. <TouchableOpacity onPress={onClose}><Text style={styles.cancelText as any}>取消</Text></TouchableOpacity>
  46. <Text style={styles.pickerTitle as any}>设置开奖时间</Text>
  47. <TouchableOpacity onPress={() => onConfirm(`${selDate} ${selHour}:${selMin}:00`)}>
  48. <Text style={styles.confirmText as any}>确定</Text>
  49. </TouchableOpacity>
  50. </View>
  51. <View style={styles.pickerBody as any}>
  52. <FlatList
  53. data={dates}
  54. keyExtractor={i => i}
  55. style={{ flex: 2 }}
  56. renderItem={({ item }) => (
  57. <TouchableOpacity
  58. style={[styles.pickerItem as any, selDate === item && styles.pickerItemActive]}
  59. onPress={() => setSelDate(item)}
  60. >
  61. <Text style={[styles.pickerItemText as any, selDate === item && styles.pickerItemTextActive]}>{item}</Text>
  62. </TouchableOpacity>
  63. )}
  64. />
  65. <FlatList
  66. data={hours}
  67. keyExtractor={i => i}
  68. style={{ flex: 1 }}
  69. renderItem={({ item }) => (
  70. <TouchableOpacity
  71. style={[styles.pickerItem as any, selHour === item && styles.pickerItemActive]}
  72. onPress={() => setSelHour(item)}
  73. >
  74. <Text style={[styles.pickerItemText as any, selHour === item && styles.pickerItemTextActive]}>{item}时</Text>
  75. </TouchableOpacity>
  76. )}
  77. />
  78. <FlatList
  79. data={mins}
  80. keyExtractor={i => i}
  81. style={{ flex: 1 }}
  82. renderItem={({ item }) => (
  83. <TouchableOpacity
  84. style={[styles.pickerItem as any, selMin === item && styles.pickerItemActive]}
  85. onPress={() => setSelMin(item)}
  86. >
  87. <Text style={[styles.pickerItemText as any, selMin === item && styles.pickerItemTextActive]}>{item}分</Text>
  88. </TouchableOpacity>
  89. )}
  90. />
  91. </View>
  92. </TouchableOpacity>
  93. </TouchableOpacity>
  94. </Modal>
  95. );
  96. };
  97. const CreateScreen = () => {
  98. const router = useRouter();
  99. const insets = useSafeAreaInsets();
  100. const [name, setName] = useState('');
  101. const [description, setDescription] = useState('');
  102. const [password, setPassword] = useState('');
  103. const [mode, setMode] = useState(0); // 0: 进房即参加, 1: 审核后参加
  104. const [luckTime, setLuckTime] = useState('');
  105. const [goodsList, setGoodsList] = useState<any[]>([]);
  106. const [checked, setChecked] = useState(true);
  107. const [showPicker, setShowPicker] = useState(false);
  108. useEffect(() => {
  109. const subscription = event.on(event.keys.STORE_CHOOSE, (goods: any[]) => {
  110. setGoodsList(prev => {
  111. // 过滤掉已经存在的,避免重复添加
  112. const existingIds = prev.map(g => g.id);
  113. const uniqueNewGoods = goods.filter(g => !existingIds.includes(g.id));
  114. return [...prev, ...uniqueNewGoods];
  115. });
  116. });
  117. return () => subscription.remove();
  118. }, []);
  119. const handleSubmit = async () => {
  120. if (!name || !description || !luckTime) {
  121. Alert.alert('提示', '请完善表单');
  122. return;
  123. }
  124. const selectedTime = new Date(luckTime.replace(/-/g, '/'));
  125. if (selectedTime <= new Date()) {
  126. Alert.alert('提示', '开奖时间必须在当前时间之后');
  127. return;
  128. }
  129. if (goodsList.length === 0) {
  130. Alert.alert('提示', '请完善赠品');
  131. return;
  132. }
  133. if (!checked) {
  134. Alert.alert('提示', '请阅读并同意协议');
  135. return;
  136. }
  137. Alert.alert('确定', '确定要创建房间吗?', [
  138. { text: '取消', style: 'cancel' },
  139. {
  140. text: '确定',
  141. onPress: async () => {
  142. const params: any = {
  143. description,
  144. inventoryIds: goodsList.map(g => g.id),
  145. luckTime,
  146. name,
  147. password,
  148. };
  149. if (password) {
  150. params.participateMode = mode;
  151. }
  152. try {
  153. const res: any = await createRoom(params);
  154. if (res && res.success) {
  155. Alert.alert('成功', '创建成功');
  156. router.back();
  157. } else {
  158. Alert.alert('创建失败', res?.msg || '操作失败');
  159. }
  160. } catch (error) {
  161. Alert.alert('错误', '发生未知错误');
  162. }
  163. }
  164. }
  165. ]);
  166. };
  167. const handleAddGoods = () => {
  168. router.push('/dimension/store_choose');
  169. };
  170. const handleRemoveGoods = (index: number) => {
  171. const newList = [...goodsList];
  172. newList.splice(index, 1);
  173. setGoodsList(newList);
  174. };
  175. return (
  176. <View style={styles.container}>
  177. <ImageBackground
  178. source={{ uri: Images.common.commonBg }}
  179. style={styles.background}
  180. resizeMode="cover"
  181. >
  182. <View style={[styles.header, { paddingTop: insets.top }]}>
  183. <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
  184. <Ionicons name="chevron-back" size={24} color="#fff" />
  185. </TouchableOpacity>
  186. <Text style={styles.title}>创建房间</Text>
  187. <View style={styles.placeholder} />
  188. </View>
  189. <ScrollView contentContainerStyle={styles.scrollContent}>
  190. <View style={styles.tipsBar}>
  191. <Ionicons name="information-circle" size={16} color="#fff" />
  192. <Text style={styles.tipsText}>房间名称及房间介绍不得带有宣传与本平台无关的内容</Text>
  193. </View>
  194. <View style={styles.formContainer}>
  195. <View style={styles.formItem}>
  196. <Text style={styles.label}>房间名:</Text>
  197. <TextInput
  198. style={styles.input}
  199. placeholder="输入房间名称,最多15个汉字"
  200. placeholderTextColor="#777"
  201. maxLength={15}
  202. value={name}
  203. onChangeText={setName}
  204. />
  205. </View>
  206. <View style={styles.formItem}>
  207. <Text style={styles.label}>活动介绍:</Text>
  208. <TextInput
  209. style={styles.input}
  210. placeholder="输入活动介绍,最多15个汉字"
  211. placeholderTextColor="#777"
  212. maxLength={15}
  213. value={description}
  214. onChangeText={setDescription}
  215. />
  216. </View>
  217. <View style={styles.formItem}>
  218. <Text style={styles.label}>进房口令:</Text>
  219. <TextInput
  220. style={styles.input}
  221. placeholder="(可选)用户输入口令才能进房间"
  222. placeholderTextColor="#777"
  223. maxLength={15}
  224. value={password}
  225. onChangeText={setPassword}
  226. />
  227. </View>
  228. {!!password && (
  229. <View style={styles.formItem}>
  230. <Text style={styles.label}>参加模式:</Text>
  231. <View style={styles.radioGroup}>
  232. <TouchableOpacity style={styles.radioItem} onPress={() => setMode(0)}>
  233. <Ionicons name={mode === 0 ? "radio-button-on" : "radio-button-off"} size={20} color={mode === 0 ? "#F8D668" : "#fff"} />
  234. <Text style={styles.radioText}>进房即参加</Text>
  235. </TouchableOpacity>
  236. <TouchableOpacity style={styles.radioItem} onPress={() => setMode(1)}>
  237. <Ionicons name={mode === 1 ? "radio-button-on" : "radio-button-off"} size={20} color={mode === 1 ? "#F8D668" : "#fff"} />
  238. <Text style={styles.radioText}>审核后参加</Text>
  239. </TouchableOpacity>
  240. </View>
  241. </View>
  242. )}
  243. <TouchableOpacity
  244. style={styles.formItem as any}
  245. onPress={() => setShowPicker(true)}
  246. >
  247. <Text style={styles.label}>开奖时间:</Text>
  248. <Text style={[styles.inputValue, !luckTime && styles.placeholderText]}>
  249. {luckTime || '设置开奖时间(一个月内)'}
  250. </Text>
  251. </TouchableOpacity>
  252. <View style={styles.goodsSection as any}>
  253. <Text style={styles.label}>赠品:</Text>
  254. <View style={styles.goodsList as any}>
  255. {goodsList.map((item, index) => (
  256. <View key={item.id} style={styles.goodsItem as any}>
  257. <Image source={{ uri: item.spu.cover }} style={styles.goodsImg} />
  258. <TouchableOpacity
  259. style={styles.removeBtn as any}
  260. onPress={() => handleRemoveGoods(index)}
  261. >
  262. <Ionicons name="close-circle" size={20} color="#ff4d4f" />
  263. </TouchableOpacity>
  264. </View>
  265. ))}
  266. <TouchableOpacity style={styles.addBtn as any} onPress={handleAddGoods}>
  267. <Ionicons name="add" size={40} color="rgba(255,255,255,0.5)" />
  268. </TouchableOpacity>
  269. </View>
  270. </View>
  271. </View>
  272. </ScrollView>
  273. <DateTimePickerModal
  274. visible={showPicker}
  275. onClose={() => setShowPicker(false)}
  276. onConfirm={(val: string) => {
  277. setLuckTime(val);
  278. setShowPicker(false);
  279. }}
  280. />
  281. <View style={[styles.footer as any, { paddingBottom: Math.max(insets.bottom, 20) }]}>
  282. <View style={styles.agreementRow as any}>
  283. <Text style={styles.agreementText}>阅读并同意</Text>
  284. <TouchableOpacity><Text style={styles.agreementLink}>《福利房使用协议》</Text></TouchableOpacity>
  285. <Switch
  286. value={checked}
  287. onValueChange={setChecked}
  288. trackColor={{ false: "#767577", true: "#81b0ff" }}
  289. thumbColor={checked ? "#f5dd4b" : "#f4f3f4"}
  290. style={styles.switch}
  291. />
  292. </View>
  293. <TouchableOpacity style={styles.submitBtn as any} onPress={handleSubmit}>
  294. <ImageBackground
  295. source={{ uri: Images.common.butBgL }}
  296. style={styles.submitBtnBg as any}
  297. resizeMode="stretch"
  298. >
  299. <Text style={styles.submitBtnText}>确认创建</Text>
  300. </ImageBackground>
  301. </TouchableOpacity>
  302. </View>
  303. </ImageBackground>
  304. </View>
  305. );
  306. };
  307. const styles = StyleSheet.create({
  308. container: {
  309. flex: 1,
  310. },
  311. background: {
  312. flex: 1,
  313. },
  314. header: {
  315. flexDirection: 'row',
  316. alignItems: 'center',
  317. justifyContent: 'space-between',
  318. paddingHorizontal: 15,
  319. height: 90,
  320. },
  321. backBtn: {
  322. width: 40,
  323. height: 40,
  324. justifyContent: 'center',
  325. },
  326. title: {
  327. fontSize: 18,
  328. fontWeight: 'bold',
  329. color: '#fff',
  330. },
  331. placeholder: {
  332. width: 40,
  333. },
  334. scrollContent: {
  335. paddingBottom: 200,
  336. },
  337. tipsBar: {
  338. backgroundColor: '#209AE5',
  339. flexDirection: 'row',
  340. alignItems: 'center',
  341. padding: 10,
  342. paddingHorizontal: 15,
  343. },
  344. tipsText: {
  345. color: '#fff',
  346. fontSize: 12,
  347. marginLeft: 8,
  348. },
  349. formContainer: {
  350. backgroundColor: 'rgba(255, 255, 255, 0.1)',
  351. margin: 15,
  352. padding: 15,
  353. borderRadius: 8,
  354. },
  355. formItem: {
  356. flexDirection: 'row',
  357. alignItems: 'center',
  358. paddingVertical: 15,
  359. borderBottomWidth: 1,
  360. borderBottomColor: 'rgba(255,255,255,0.1)',
  361. },
  362. label: {
  363. width: 80,
  364. fontSize: 14,
  365. color: '#fff',
  366. },
  367. input: {
  368. flex: 1,
  369. fontSize: 14,
  370. color: '#fff',
  371. padding: 0,
  372. },
  373. inputValue: {
  374. flex: 1,
  375. fontSize: 14,
  376. color: '#fff',
  377. },
  378. placeholderText: {
  379. color: '#777',
  380. },
  381. radioGroup: {
  382. flex: 1,
  383. flexDirection: 'row',
  384. justifyContent: 'space-around',
  385. },
  386. radioItem: {
  387. flexDirection: 'row',
  388. alignItems: 'center',
  389. },
  390. radioText: {
  391. color: '#fff',
  392. fontSize: 12,
  393. marginLeft: 5,
  394. },
  395. goodsSection: {
  396. paddingVertical: 15,
  397. },
  398. goodsList: {
  399. flexDirection: 'row',
  400. flexWrap: 'wrap',
  401. marginTop: 10,
  402. },
  403. goodsItem: {
  404. width: 80,
  405. height: 80,
  406. marginRight: 10,
  407. marginBottom: 10,
  408. backgroundColor: 'rgba(255,255,255,0.1)',
  409. borderRadius: 8,
  410. padding: 5,
  411. position: 'relative',
  412. },
  413. goodsImg: {
  414. width: '100%',
  415. height: '100%',
  416. borderRadius: 4,
  417. },
  418. removeBtn: {
  419. position: 'absolute',
  420. top: -8,
  421. right: -8,
  422. },
  423. addBtn: {
  424. width: 80,
  425. height: 80,
  426. backgroundColor: 'rgba(255,255,255,0.1)',
  427. borderRadius: 8,
  428. justifyContent: 'center',
  429. alignItems: 'center',
  430. },
  431. footer: {
  432. position: 'absolute',
  433. bottom: 0,
  434. left: 0,
  435. right: 0,
  436. alignItems: 'center',
  437. paddingTop: 10,
  438. backgroundColor: 'rgba(0,0,0,0.5)',
  439. },
  440. agreementRow: {
  441. flexDirection: 'row',
  442. alignItems: 'center',
  443. marginBottom: 10,
  444. },
  445. agreementText: {
  446. color: '#fff',
  447. fontSize: 12,
  448. },
  449. agreementLink: {
  450. color: '#50DAFF',
  451. fontSize: 12,
  452. },
  453. switch: {
  454. transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }],
  455. marginLeft: 5,
  456. },
  457. submitBtn: {
  458. width: 200,
  459. height: 60,
  460. },
  461. submitBtnBg: {
  462. width: '100%',
  463. height: '100%',
  464. justifyContent: 'center',
  465. alignItems: 'center',
  466. },
  467. submitBtnText: {
  468. fontSize: 16,
  469. fontWeight: '800',
  470. color: '#fff',
  471. },
  472. modalOverlay: {
  473. flex: 1,
  474. backgroundColor: 'rgba(0,0,0,0.6)',
  475. justifyContent: 'flex-end',
  476. },
  477. pickerContent: {
  478. backgroundColor: '#fff',
  479. borderTopLeftRadius: 20,
  480. borderTopRightRadius: 20,
  481. height: 350,
  482. },
  483. pickerHeader: {
  484. flexDirection: 'row',
  485. justifyContent: 'space-between',
  486. alignItems: 'center',
  487. padding: 15,
  488. borderBottomWidth: 1,
  489. borderBottomColor: '#eee',
  490. },
  491. pickerTitle: {
  492. fontSize: 16,
  493. fontWeight: 'bold',
  494. color: '#333',
  495. },
  496. cancelText: {
  497. color: '#999',
  498. fontSize: 14,
  499. },
  500. confirmText: {
  501. color: '#209AE5',
  502. fontSize: 14,
  503. fontWeight: 'bold',
  504. },
  505. pickerBody: {
  506. flex: 1,
  507. flexDirection: 'row',
  508. },
  509. pickerItem: {
  510. height: 44,
  511. justifyContent: 'center',
  512. alignItems: 'center',
  513. },
  514. pickerItemActive: {
  515. backgroundColor: '#f5f5f5',
  516. },
  517. pickerItemText: {
  518. fontSize: 14,
  519. color: '#666',
  520. },
  521. pickerItemTextActive: {
  522. color: '#209AE5',
  523. fontWeight: 'bold',
  524. },
  525. });
  526. export default CreateScreen;