今天我们用 React Native 实现一个星座查询工具,根据生日查询对应的星座和性格特征。
请添加图片描述

星座数据

import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Animated } from 'react-native';

const constellations = [
  { name: '白羊座', emoji: '♈', dates: '3/21-4/19', element: '火', traits: '热情、冲动、勇敢' },
  { name: '金牛座', emoji: '♉', dates: '4/20-5/20', element: '土', traits: '稳重、务实、固执' },
  { name: '双子座', emoji: '♊', dates: '5/21-6/21', element: '风', traits: '机智、善变、好奇' },
  { name: '巨蟹座', emoji: '♋', dates: '6/22-7/22', element: '水', traits: '敏感、顾家、情绪化' },
  { name: '狮子座', emoji: '♌', dates: '7/23-8/22', element: '火', traits: '自信、慷慨、爱面子' },
  { name: '处女座', emoji: '♍', dates: '8/23-9/22', element: '土', traits: '完美主义、细心、挑剔' },
  { name: '天秤座', emoji: '♎', dates: '9/23-10/23', element: '风', traits: '优雅、公正、犹豫' },
  { name: '天蝎座', emoji: '♏', dates: '10/24-11/22', element: '水', traits: '神秘、专注、记仇' },
  { name: '射手座', emoji: '♐', dates: '11/23-12/21', element: '火', traits: '乐观、自由、直率' },
  { name: '摩羯座', emoji: '♑', dates: '12/22-1/19', element: '土', traits: '务实、有野心、保守' },
  { name: '水瓶座', emoji: '♒', dates: '1/20-2/18', element: '风', traits: '独立、创新、叛逆' },
  { name: '双鱼座', emoji: '♓', dates: '2/19-3/20', element: '水', traits: '浪漫、敏感、逃避' },
];

星座数据数组,包含 12 个星座的信息。

每个星座的属性

  • name:星座名称
  • emoji:星座符号
  • dates:日期范围
  • element:元素(火、土、风、水)
  • traits:性格特征

为什么用元素分类?因为西方占星学把 12 星座分成 4 个元素:火象(白羊、狮子、射手)、土象(金牛、处女、摩羯)、风象(双子、天秤、水瓶)、水象(巨蟹、天蝎、双鱼)。每个元素有不同的性格特点。

状态设计

export const Constellation: React.FC = () => {
  const [month, setMonth] = useState('');
  const [day, setDay] = useState('');
  const [result, setResult] = useState<typeof constellations[0] | null>(null);
  
  const scaleAnim = useRef(new Animated.Value(0)).current;
  const rotateAnim = useRef(new Animated.Value(0)).current;
  const gridAnims = useRef(constellations.map(() => new Animated.Value(0))).current;

状态设计包含月份、日期、查询结果、动画值。

月份和日期monthday 都是字符串类型,存储用户输入的月份和日期。

查询结果result 是星座对象或 null

三个动画值

  • scaleAnim:结果卡片的缩放动画
  • rotateAnim:星座符号的旋转动画
  • gridAnims:星座网格的动画数组,12 个元素

网格动画初始化

  useEffect(() => {
    gridAnims.forEach((anim, index) => {
      Animated.timing(anim, { toValue: 1, duration: 300, delay: index * 40, useNativeDriver: true }).start();
    });
  }, []);

组件挂载时,初始化星座网格的动画。每个星座延迟 index * 40 毫秒启动,持续 300ms,依次出现。

星座计算

  const getConstellation = (m: number, d: number) => {
    const dates = [[20, 19], [19, 20], [21, 20], [20, 21], [21, 22], [22, 23], [23, 23], [23, 23], [23, 22], [24, 22], [23, 21], [22, 20]];
    const index = d < dates[m - 1][0] ? (m + 10) % 12 : (m + 11) % 12;
    return constellations[index];
  };

根据月份和日期计算星座。

日期分界点数组dates 是一个二维数组,每个元素是 [前一个星座的结束日, 当前星座的结束日]

计算索引

  • 如果日期小于分界点,属于前一个星座:(m + 10) % 12
  • 否则属于当前星座:(m + 11) % 12

为什么用这个公式?因为星座是按月份顺序排列的,但每个星座的起始日期不同。用分界点判断,再用取余运算处理跨年的情况(12 月到 1 月)。

举例:计算 3 月 25 日的星座。

  • m = 3, d = 25
  • dates[2] = [21, 20](3 月的分界点是 21 日)
  • d (25) >= dates[2][0] (21),属于当前星座
  • index = (3 + 11) % 12 = 2
  • constellations[2] = { name: ‘双子座’, … }

等等,这个例子有问题。让我重新理解:3 月 25 日应该是白羊座(3/21-4/19)。

实际上,dates 数组的含义是:

  • dates[0] = [20, 19]:1 月 20 日是水瓶座的开始,1 月 19 日是摩羯座的结束
  • dates[1] = [19, 20]:2 月 19 日是双鱼座的开始
  • dates[2] = [21, 20]:3 月 21 日是白羊座的开始

如果 d < dates[m-1][0],说明还在前一个星座。

查询函数

  const search = () => {
    const m = parseInt(month);
    const d = parseInt(day);
    if (m >= 1 && m <= 12 && d >= 1 && d <= 31) {
      scaleAnim.setValue(0);
      rotateAnim.setValue(0);
      setResult(getConstellation(m, d));
      Animated.parallel([
        Animated.spring(scaleAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
        Animated.timing(rotateAnim, { toValue: 1, duration: 500, useNativeDriver: true }),
      ]).start();
    }
  };

查询按钮点击时,计算星座,触发动画。

解析输入parseInt() 把字符串转成整数。

验证输入:月份 1-12,日期 1-31。

重置动画值:把缩放和旋转动画值都设为 0。

设置结果:调用 getConstellation(m, d) 计算星座。

并行动画:缩放动画(弹簧)和旋转动画(500ms)同时触发。

选择星座

  const selectConstellation = (c: typeof constellations[0]) => {
    scaleAnim.setValue(0);
    rotateAnim.setValue(0);
    setResult(c);
    Animated.parallel([
      Animated.spring(scaleAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
      Animated.timing(rotateAnim, { toValue: 1, duration: 500, useNativeDriver: true }),
    ]).start();
  };

  const spin = rotateAnim.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'] });

点击星座网格时,直接显示该星座的信息。逻辑同查询函数。

旋转插值:把动画值 0-1 映射到旋转角度 0-360 度。

元素颜色

  const getElementColor = (element: string) => {
    switch (element) {
      case '火': return '#e74c3c';
      case '土': return '#8b4513';
      case '风': return '#3498db';
      case '水': return '#1abc9c';
      default: return '#4A90D9';
    }
  };

根据元素返回对应的颜色。

四种颜色

  • 火:红色 #e74c3c
  • 土:棕色 #8b4513
  • 风:蓝色 #3498db
  • 水:青色 #1abc9c

为什么用不同颜色?因为颜色能直观地表达元素的特性。火是红色(热情),土是棕色(稳重),风是蓝色(自由),水是青色(流动)。

界面渲染:头部和输入

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerEmoji}>⭐</Text>
        <Text style={styles.headerTitle}>星座查询</Text>
        <Text style={styles.headerSubtitle}>探索你的星座</Text>
      </View>

      <View style={styles.inputSection}>
        <View style={styles.inputGroup}>
          <Text style={styles.label}>月</Text>
          <TextInput style={styles.input} value={month} onChangeText={setMonth} keyboardType="numeric" placeholder="1-12" placeholderTextColor="#666" />
        </View>
        <View style={styles.inputGroup}>
          <Text style={styles.label}>日</Text>
          <TextInput style={styles.input} value={day} onChangeText={setDay} keyboardType="numeric" placeholder="1-31" placeholderTextColor="#666" />
        </View>
        <TouchableOpacity style={styles.btn} onPress={search} activeOpacity={0.8}>
          <Text style={styles.btnText}>查询</Text>
        </TouchableOpacity>
      </View>

头部显示标题和副标题,输入区域包含月份、日期输入框和查询按钮。

头部

  • 图标:⭐ 星星
  • 标题:星座查询
  • 副标题:探索你的星座

输入区域

  • 横向布局,底部对齐
  • 月份输入框:标签"月",占位符"1-12"
  • 日期输入框:标签"日",占位符"1-31"
  • 查询按钮:紫色背景,带阴影

为什么用紫色按钮?因为紫色代表神秘、浪漫,符合星座查询的主题。

结果显示

      {result && (
        <Animated.View style={[styles.result, { transform: [{ scale: scaleAnim }], borderColor: getElementColor(result.element) }]}>
          <Animated.Text style={[styles.emoji, { transform: [{ rotate: spin }] }]}>{result.emoji}</Animated.Text>
          <Text style={styles.name}>{result.name}</Text>
          <Text style={styles.dates}>{result.dates}</Text>
          <View style={styles.infoRow}>
            <Text style={styles.infoLabel}>元素</Text>
            <Text style={[styles.infoValue, { color: getElementColor(result.element) }]}>{result.element}象星座</Text>
          </View>
          <View style={styles.infoRow}>
            <Text style={styles.infoLabel}>特点</Text>
            <Text style={styles.infoValue}>{result.traits}</Text>
          </View>
        </Animated.View>
      )}

结果卡片显示星座信息。

条件渲染:只有 result 不为 null 时才显示。

卡片动画:缩放从 0 到 1。

边框颜色:根据元素动态设置,火象红色,土象棕色,风象蓝色,水象青色。

星座符号

  • 字号 60
  • 应用旋转动画,旋转一圈

星座名称:白色文字,字号 28,加粗。

日期范围:灰色文字。

信息行

  • 元素:标签"元素",值"X象星座",颜色根据元素动态设置
  • 特点:标签"特点",值是性格特征

为什么边框颜色根据元素变化?因为颜色能快速传达元素信息。用户看到红色边框,就知道是火象星座。看到蓝色边框,就知道是风象星座。

星座网格

      <View style={styles.grid}>
        {constellations.map((c, i) => (
          <Animated.View key={i} style={{ opacity: gridAnims[i], transform: [{ scale: gridAnims[i] }] }}>
            <TouchableOpacity style={[styles.gridItem, result?.name === c.name && styles.gridItemActive]} onPress={() => selectConstellation(c)} activeOpacity={0.7}>
              <Text style={styles.gridEmoji}>{c.emoji}</Text>
              <Text style={[styles.gridName, result?.name === c.name && styles.gridNameActive]}>{c.name}</Text>
              <Text style={styles.gridDates}>{c.dates}</Text>
            </TouchableOpacity>
          </Animated.View>
        ))}
      </View>
    </ScrollView>
  );
};

星座网格显示所有 12 个星座。

网格布局flexDirection: 'row'flexWrap: 'wrap' 让卡片自动换行。

卡片动画:透明度和缩放从 0 到 1。

卡片内容

  • 星座符号:字号 28
  • 星座名称:白色,字号 12
  • 日期范围:灰色,字号 10

激活状态

  • 选中的卡片用紫色背景,带阴影
  • 选中的卡片名称加粗

为什么显示日期范围?因为用户可能不记得自己的星座,看到日期范围能快速找到。比如"我是 3 月 25 日出生,看到白羊座是 3/21-4/19,就知道我是白羊座"。

鸿蒙 ArkTS 对比:星座计算

@State month: string = ''
@State day: string = ''
@State result: any = null

getConstellation(m: number, d: number) {
  const dates = [[20, 19], [19, 20], [21, 20], [20, 21], [21, 22], [22, 23], [23, 23], [23, 23], [23, 22], [24, 22], [23, 21], [22, 20]]
  const index = d < dates[m - 1][0] ? (m + 10) % 12 : (m + 11) % 12
  return constellations[index]
}

search() {
  const m = parseInt(this.month)
  const d = parseInt(this.day)
  if (m >= 1 && m <= 12 && d >= 1 && d <= 31) {
    this.result = this.getConstellation(m, d)
  }
}

getElementColor(element: string): string {
  switch (element) {
    case '火': return '#e74c3c'
    case '土': return '#8b4513'
    case '风': return '#3498db'
    case '水': return '#1abc9c'
    default: return '#4A90D9'
  }
}

ArkTS 中的星座计算逻辑完全一样。核心是日期分界点判断和取余运算。parseInt()switch 都是标准 JavaScript 语法,跨平台通用。

样式定义

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0f0f23', padding: 16 },
  header: { alignItems: 'center', marginBottom: 24 },
  headerEmoji: { fontSize: 50, marginBottom: 8 },
  headerTitle: { fontSize: 28, fontWeight: '700', color: '#fff' },
  headerSubtitle: { fontSize: 14, color: '#888', marginTop: 4 },
  inputSection: { flexDirection: 'row', marginBottom: 20, alignItems: 'flex-end' },
  inputGroup: { flex: 1, marginRight: 8 },
  label: { fontSize: 12, color: '#888', marginBottom: 4 },
  input: { backgroundColor: '#1a1a3e', color: '#fff', padding: 14, borderRadius: 12, fontSize: 18, textAlign: 'center', borderWidth: 1, borderColor: '#3a3a6a' },
  btn: { backgroundColor: '#6c5ce7', paddingVertical: 14, paddingHorizontal: 24, borderRadius: 12, shadowColor: '#6c5ce7', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.4, shadowRadius: 8, elevation: 6 },
  btnText: { color: '#fff', fontWeight: '700' },
  result: { backgroundColor: '#1a1a3e', padding: 24, borderRadius: 20, alignItems: 'center', marginBottom: 24, borderWidth: 2, shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.4, shadowRadius: 16, elevation: 10 },
  emoji: { fontSize: 60, marginBottom: 8 },
  name: { fontSize: 28, fontWeight: '700', color: '#fff', marginVertical: 8 },
  dates: { color: '#888', marginBottom: 16 },
  infoRow: { flexDirection: 'row', width: '100%', paddingVertical: 10, borderTopWidth: 1, borderTopColor: '#3a3a6a' },
  infoLabel: { color: '#888', width: 60 },
  infoValue: { color: '#fff', flex: 1 },
  grid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center' },
  gridItem: { width: 100, margin: 4, backgroundColor: '#1a1a3e', padding: 12, borderRadius: 16, alignItems: 'center', borderWidth: 1, borderColor: '#3a3a6a' },
  gridItemActive: { backgroundColor: '#6c5ce7', borderColor: '#6c5ce7', shadowColor: '#6c5ce7', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.5, shadowRadius: 8, elevation: 6 },
  gridEmoji: { fontSize: 28 },
  gridName: { fontSize: 12, color: '#fff', marginTop: 4 },
  gridNameActive: { fontWeight: '600' },
  gridDates: { fontSize: 10, color: '#666' },
});

容器用深蓝黑色背景。输入区域横向布局,底部对齐。按钮用紫色背景,带阴影。结果卡片边框颜色根据元素动态设置。星座网格每个卡片宽度 100。选中的卡片用紫色背景,带阴影。

小结

这个星座查询工具展示了星座计算和元素分类的实现。用日期分界点判断星座,根据元素设置不同颜色。旋转动画让星座符号旋转一圈,网格布局显示 12 个星座。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐