|
@@ -10,377 +10,171 @@ import {ref,reactive} from "vue"
|
|
|
<style lang="scss" scoped>
|
|
|
</style> -->
|
|
|
<template>
|
|
|
- <div class="weather-app">
|
|
|
-
|
|
|
- <!-- 搜索区域 -->
|
|
|
- <div class="search-container">
|
|
|
+ <div class="guess-game">
|
|
|
+ <h1>数字猜谜游戏</h1>
|
|
|
+ <p>请猜一个 1-100 之间的数字</p>
|
|
|
+ <div class="game-container">
|
|
|
<input
|
|
|
- type="text"
|
|
|
- v-model="searchQuery"
|
|
|
- placeholder="输入城市名称..."
|
|
|
- @keyup.enter="searchWeather"
|
|
|
+ type="number"
|
|
|
+ v-model.number="userGuess"
|
|
|
+ min="1"
|
|
|
+ max="100"
|
|
|
+ placeholder="输入猜测的数字"
|
|
|
+ @keyup.enter="checkGuess"
|
|
|
>
|
|
|
- <button @click="searchWeather">查询</button>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 加载状态 -->
|
|
|
- <div class="loading" v-if="isLoading">
|
|
|
- 正在查询天气数据...
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 错误提示 -->
|
|
|
- <div class="error" v-if="errorMessage">
|
|
|
- ⚠️ {{ errorMessage }}
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 天气信息展示 -->
|
|
|
- <div class="weather-card" v-if="currentWeather && !isLoading && !errorMessage">
|
|
|
- <div class="weather-header">
|
|
|
- <h2>{{ currentWeather.city }}</h2>
|
|
|
- <div class="date">{{ formattedDate }}</div>
|
|
|
+ <button @click="checkGuess">提交猜测</button>
|
|
|
+ <div class="message" v-if="message">
|
|
|
+ {{ message }}
|
|
|
</div>
|
|
|
-
|
|
|
- <div class="weather-main">
|
|
|
- <div class="temperature">
|
|
|
- <span class="value">{{ currentTemp }}</span>
|
|
|
- <span class="unit" @click="toggleUnit">
|
|
|
- {{ isCelsius ? '°C' : '°F' }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <div class="condition">
|
|
|
- <div class="icon">{{ weatherIcon }}</div>
|
|
|
- <div class="text">{{ currentWeather.condition }}</div>
|
|
|
- </div>
|
|
|
+ <div class="stats">
|
|
|
+ <p>已猜次数: {{ attempts }}</p>
|
|
|
+ <button @click="resetGame">重新开始</button>
|
|
|
</div>
|
|
|
-
|
|
|
- <div class="weather-details">
|
|
|
- <div class="detail">
|
|
|
- <span class="label">湿度:</span>
|
|
|
- <span class="value">{{ currentWeather.humidity }}%</span>
|
|
|
- </div>
|
|
|
- <div class="detail">
|
|
|
- <span class="label">风速:</span>
|
|
|
- <span class="value">{{ currentWeather.windSpeed }} km/h</span>
|
|
|
- </div>
|
|
|
- <div class="detail">
|
|
|
- <span class="label">气压:</span>
|
|
|
- <span class="value">{{ currentWeather.pressure }} hPa</span>
|
|
|
+ <div class="history" v-if="guessHistory.length > 0">
|
|
|
+ <h3>猜测历史:</h3>
|
|
|
+ <div class="history-items">
|
|
|
+ <span
|
|
|
+ v-for="(guess, index) in guessHistory"
|
|
|
+ :key="index"
|
|
|
+ :class="{
|
|
|
+ high: guess > targetNumber,
|
|
|
+ low: guess < targetNumber,
|
|
|
+ correct: guess === targetNumber
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ {{ guess }}
|
|
|
+ </span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 历史记录 -->
|
|
|
- <div class="history-section" v-if="searchHistory.length > 0">
|
|
|
- <h3>查询历史</h3>
|
|
|
- <div class="history-tags">
|
|
|
- <span
|
|
|
- class="history-tag"
|
|
|
- v-for="(item, index) in searchHistory"
|
|
|
- :key="index"
|
|
|
- @click="searchWeather(item)"
|
|
|
- >
|
|
|
- {{ item }}
|
|
|
- <button @click.stop="removeHistory(index)" class="remove-btn">×</button>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
-
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, computed, watch } from 'vue';
|
|
|
-
|
|
|
-// 1. 定义天气数据类型接口
|
|
|
-interface WeatherData {
|
|
|
- city: string;
|
|
|
- temperature: number; // 摄氏度
|
|
|
- condition: string; // 天气状况
|
|
|
- humidity: number; // 湿度百分比
|
|
|
- windSpeed: number; // 风速 km/h
|
|
|
- pressure: number; // 气压 hPa
|
|
|
- date: Date; // 日期
|
|
|
-}
|
|
|
-
|
|
|
-// 2. 响应式状态
|
|
|
-const searchQuery = ref<string>('');
|
|
|
-const currentWeather = ref<WeatherData | null>(null);
|
|
|
-const isLoading = ref<boolean>(false);
|
|
|
-const errorMessage = ref<string>('');
|
|
|
-const isCelsius = ref<boolean>(true);
|
|
|
-const searchHistory = ref<string[]>([]);
|
|
|
-
|
|
|
-// 3. 温度单位转换计算属性
|
|
|
-const currentTemp = computed<number>(() => {
|
|
|
- if (!currentWeather.value) return 0;
|
|
|
-
|
|
|
- // 如果是华氏度,进行转换 (°F = °C × 1.8 + 32)
|
|
|
- return isCelsius.value
|
|
|
- ? currentWeather.value.temperature
|
|
|
- : Math.round((currentWeather.value.temperature * 1.8 + 32) * 10) / 10;
|
|
|
-});
|
|
|
-
|
|
|
-// 4. 格式化日期
|
|
|
-const formattedDate = computed<string>(() => {
|
|
|
- if (!currentWeather.value) return '';
|
|
|
-
|
|
|
- return currentWeather.value.date.toLocaleDateString('zh-CN', {
|
|
|
- year: 'numeric',
|
|
|
- month: 'long',
|
|
|
- day: 'numeric',
|
|
|
- weekday: 'long'
|
|
|
- });
|
|
|
-});
|
|
|
-
|
|
|
-// 5. 根据天气状况显示对应图标
|
|
|
-const weatherIcon = computed<string>(() => {
|
|
|
- if (!currentWeather.value) return '';
|
|
|
-
|
|
|
- const condition = currentWeather.value.condition.toLowerCase();
|
|
|
-
|
|
|
- if (condition.includes('晴')) return '☀️';
|
|
|
- if (condition.includes('雨')) return '🌧️';
|
|
|
- if (condition.includes('云')) return '☁️';
|
|
|
- if (condition.includes('雪')) return '❄️';
|
|
|
- if (condition.includes('雷')) return '⛈️';
|
|
|
- return '🌤️';
|
|
|
-});
|
|
|
-
|
|
|
-// 6. 切换温度单位
|
|
|
-const toggleUnit = () => {
|
|
|
- isCelsius.value = !isCelsius.value;
|
|
|
+import { ref } from 'vue';
|
|
|
+// 生成1-100之间的随机目标数字
|
|
|
+const generateTargetNumber = (): number => {
|
|
|
+ return Math.floor(Math.random() * 100) + 1;
|
|
|
};
|
|
|
-
|
|
|
-// 7. 模拟天气数据查询(实际项目中会调用API)
|
|
|
-const searchWeather = async (city?: string) => {
|
|
|
- // 清空错误信息
|
|
|
- errorMessage.value = '';
|
|
|
-
|
|
|
- // 确定要查询的城市
|
|
|
- const targetCity = city || searchQuery.value.trim();
|
|
|
- if (!targetCity) {
|
|
|
- errorMessage.value = '请输入城市名称';
|
|
|
+// 响应式状态定义(均指定类型)
|
|
|
+const targetNumber = ref<number>(generateTargetNumber());
|
|
|
+const userGuess = ref<number | null>(null);
|
|
|
+const message = ref<string>('');
|
|
|
+const attempts = ref<number>(0);
|
|
|
+const guessHistory = ref<number[]>([]);
|
|
|
+// 检查猜测结果
|
|
|
+const checkGuess = () => {
|
|
|
+ // 验证输入有效性
|
|
|
+ if (userGuess.value === null || userGuess.value < 1 || userGuess.value > 100) {
|
|
|
+ message.value = '请输入1-100之间的有效数字!';
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- try {
|
|
|
- isLoading.value = true;
|
|
|
-
|
|
|
- // 模拟API请求延迟
|
|
|
- await new Promise(resolve => setTimeout(resolve, 800));
|
|
|
-
|
|
|
- // 模拟返回的天气数据(实际项目中从API获取)
|
|
|
- const mockWeatherData: WeatherData = {
|
|
|
- city: targetCity,
|
|
|
- temperature: Math.floor(Math.random() * 40) - 10, // -10到30度之间
|
|
|
- condition: ['晴朗', '多云', '小雨', '中雨', '雷阵雨', '小雪'][Math.floor(Math.random() * 6)],
|
|
|
- humidity: Math.floor(Math.random() * 50) + 30, // 30%到80%
|
|
|
- windSpeed: Math.floor(Math.random() * 20) + 1, // 1到20 km/h
|
|
|
- pressure: Math.floor(Math.random() * 50) + 1000, // 1000到1050 hPa
|
|
|
- date: new Date()
|
|
|
- };
|
|
|
-
|
|
|
- currentWeather.value = mockWeatherData;
|
|
|
-
|
|
|
- // 添加到历史记录(去重)
|
|
|
- if (!searchHistory.value.includes(targetCity)) {
|
|
|
- searchHistory.value.unshift(targetCity);
|
|
|
- // 限制历史记录数量
|
|
|
- if (searchHistory.value.length > 5) {
|
|
|
- searchHistory.value.pop();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 清空搜索框
|
|
|
- searchQuery.value = '';
|
|
|
- } catch (err) {
|
|
|
- errorMessage.value = '查询天气失败,请重试';
|
|
|
- console.error('天气查询错误:', err);
|
|
|
- } finally {
|
|
|
- isLoading.value = false;
|
|
|
+ // 记录猜测次数和历史
|
|
|
+ attempts.value++;
|
|
|
+ guessHistory.value.push(userGuess.value);
|
|
|
+ // 判断猜测结果
|
|
|
+ if (userGuess.value === targetNumber.value) {
|
|
|
+ message.value = `恭喜你猜对了! 答案就是 ${targetNumber.value},共用了 ${attempts.value} 次`;
|
|
|
+ } else if (userGuess.value > targetNumber.value) {
|
|
|
+ message.value = '猜大了,再试试小的数字!';
|
|
|
+ } else {
|
|
|
+ message.value = '猜小了,再试试大的数字!';
|
|
|
}
|
|
|
+ // 清空输入框
|
|
|
+ userGuess.value = null;
|
|
|
};
|
|
|
-
|
|
|
-// 8. 移除历史记录
|
|
|
-const removeHistory = (index: number) => {
|
|
|
- searchHistory.value.splice(index, 1);
|
|
|
+// 重置游戏
|
|
|
+const resetGame = () => {
|
|
|
+ targetNumber.value = generateTargetNumber();
|
|
|
+ userGuess.value = null;
|
|
|
+ message.value = '';
|
|
|
+ attempts.value = 0;
|
|
|
+ guessHistory.value = [];
|
|
|
};
|
|
|
</script>
|
|
|
-
|
|
|
<style scoped>
|
|
|
-.weather-app {
|
|
|
- max-width: 600px;
|
|
|
+.guess-game {
|
|
|
+ max-width: 500px;
|
|
|
margin: 2rem auto;
|
|
|
padding: 0 1rem;
|
|
|
- font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
|
+ font-family: 'Arial', sans-serif;
|
|
|
+ text-align: center;
|
|
|
}
|
|
|
-
|
|
|
-.search-container {
|
|
|
- display: flex;
|
|
|
- gap: 10px;
|
|
|
- margin: 2rem 0;
|
|
|
+.game-container {
|
|
|
+ margin-top: 2rem;
|
|
|
+ padding: 2rem;
|
|
|
+ background-color: #f8fafc;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
|
}
|
|
|
-
|
|
|
-.search-container input {
|
|
|
- flex: 1;
|
|
|
- padding: 10px;
|
|
|
- border: 1px solid #e2e8f0;
|
|
|
+input {
|
|
|
+ padding: 0.75rem;
|
|
|
+ width: 150px;
|
|
|
+ margin-right: 0.5rem;
|
|
|
+ border: 1px solid #cbd5e1;
|
|
|
border-radius: 4px;
|
|
|
font-size: 1rem;
|
|
|
}
|
|
|
-
|
|
|
-.search-container button {
|
|
|
- padding: 10px 20px;
|
|
|
- background-color: #3182ce;
|
|
|
+button {
|
|
|
+ padding: 0.75rem 1.5rem;
|
|
|
+ background-color: #3b82f6;
|
|
|
color: white;
|
|
|
border: none;
|
|
|
border-radius: 4px;
|
|
|
cursor: pointer;
|
|
|
+ font-size: 1rem;
|
|
|
+ transition: background-color 0.2s;
|
|
|
}
|
|
|
-
|
|
|
-.loading, .error {
|
|
|
- text-align: center;
|
|
|
+button:hover {
|
|
|
+ background-color: #2563eb;
|
|
|
+}
|
|
|
+.message {
|
|
|
+ margin: 1.5rem 0;
|
|
|
padding: 1rem;
|
|
|
border-radius: 4px;
|
|
|
- margin-bottom: 1rem;
|
|
|
+ font-size: 1.1rem;
|
|
|
+ min-height: 48px;
|
|
|
}
|
|
|
-
|
|
|
-.loading {
|
|
|
- background-color: #ebf8ff;
|
|
|
- color: #3182ce;
|
|
|
-}
|
|
|
-
|
|
|
-.error {
|
|
|
- background-color: #fff5f5;
|
|
|
- color: #e53e3e;
|
|
|
-}
|
|
|
-
|
|
|
-.weather-card {
|
|
|
- background-color: white;
|
|
|
- border-radius: 8px;
|
|
|
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
|
- padding: 1.5rem;
|
|
|
- margin-bottom: 2rem;
|
|
|
-}
|
|
|
-
|
|
|
-.weather-header {
|
|
|
+.stats {
|
|
|
+ margin: 1rem 0;
|
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 1rem;
|
|
|
align-items: center;
|
|
|
- margin-bottom: 1.5rem;
|
|
|
- padding-bottom: 1rem;
|
|
|
- border-bottom: 1px solid #edf2f7;
|
|
|
}
|
|
|
-
|
|
|
-.weather-header h2 {
|
|
|
- margin: 0;
|
|
|
- color: #2d3748;
|
|
|
-}
|
|
|
-
|
|
|
-.date {
|
|
|
- color: #718096;
|
|
|
+.history {
|
|
|
+ margin-top: 1.5rem;
|
|
|
+ padding-top: 1.5rem;
|
|
|
+ border-top: 1px solid #e2e8f0;
|
|
|
}
|
|
|
-
|
|
|
-.weather-main {
|
|
|
+.history-items {
|
|
|
+ margin-top: 0.5rem;
|
|
|
display: flex;
|
|
|
- justify-content: space-around;
|
|
|
- align-items: center;
|
|
|
- margin-bottom: 1.5rem;
|
|
|
-}
|
|
|
-
|
|
|
-.temperature {
|
|
|
- display: flex;
|
|
|
- align-items: baseline;
|
|
|
-}
|
|
|
-
|
|
|
-.temperature .value {
|
|
|
- font-size: 4rem;
|
|
|
- font-weight: bold;
|
|
|
- color: #2d3748;
|
|
|
-}
|
|
|
-
|
|
|
-.temperature .unit {
|
|
|
- font-size: 1.5rem;
|
|
|
- color: #4a5568;
|
|
|
- cursor: pointer;
|
|
|
- margin-left: 0.5rem;
|
|
|
- text-decoration: underline;
|
|
|
-}
|
|
|
-
|
|
|
-.condition {
|
|
|
- text-align: center;
|
|
|
-}
|
|
|
-
|
|
|
-.condition .icon {
|
|
|
- font-size: 3rem;
|
|
|
- margin-bottom: 0.5rem;
|
|
|
-}
|
|
|
-
|
|
|
-.condition .text {
|
|
|
- font-size: 1.2rem;
|
|
|
- color: #4a5568;
|
|
|
-}
|
|
|
-
|
|
|
-.weather-details {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(3, 1fr);
|
|
|
- gap: 1rem;
|
|
|
- padding-top: 1rem;
|
|
|
- border-top: 1px solid #edf2f7;
|
|
|
-}
|
|
|
-
|
|
|
-.detail {
|
|
|
- text-align: center;
|
|
|
-}
|
|
|
-
|
|
|
-.detail .label {
|
|
|
- color: #718096;
|
|
|
- font-size: 0.9rem;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 0.5rem;
|
|
|
+ justify-content: center;
|
|
|
}
|
|
|
-
|
|
|
-.detail .value {
|
|
|
- color: #2d3748;
|
|
|
- font-weight: 500;
|
|
|
+.history-items span {
|
|
|
+ padding: 0.5rem;
|
|
|
+ border-radius: 4px;
|
|
|
+ background-color: #e2e8f0;
|
|
|
}
|
|
|
-
|
|
|
-.history-section {
|
|
|
- margin-top: 2rem;
|
|
|
+.history-items .high {
|
|
|
+ background-color: #fee2e2;
|
|
|
+ color: #dc2626;
|
|
|
}
|
|
|
-
|
|
|
-.history-section h3 {
|
|
|
- color: #4a5568;
|
|
|
- margin-bottom: 1rem;
|
|
|
+.history-items .low {
|
|
|
+ background-color: #dbeafe;
|
|
|
+ color: #1e40af;
|
|
|
}
|
|
|
-
|
|
|
-.history-tags {
|
|
|
- display: flex;
|
|
|
- flex-wrap: wrap;
|
|
|
- gap: 0.5rem;
|
|
|
+.history-items .correct {
|
|
|
+ background-color: #dcfce7;
|
|
|
+ color: #059669;
|
|
|
+ font-weight: bold;
|
|
|
}
|
|
|
-
|
|
|
-.history-tag {
|
|
|
- background-color: #edf2f7;
|
|
|
- padding: 0.5rem 1rem;
|
|
|
- border-radius: 20px;
|
|
|
- font-size: 0.9rem;
|
|
|
- cursor: pointer;
|
|
|
- display: inline-flex;
|
|
|
- align-items: center;
|
|
|
- gap: 0.5rem;
|
|
|
+.clear-completed-btn {
|
|
|
+ background-color: #ef4444;
|
|
|
}
|
|
|
-
|
|
|
-.remove-btn {
|
|
|
- background: none;
|
|
|
- border: none;
|
|
|
- color: #718096;
|
|
|
- cursor: pointer;
|
|
|
- font-size: 1rem;
|
|
|
- padding: 0;
|
|
|
- line-height: 1;
|
|
|
-}
|
|
|
-
|
|
|
-.remove-btn:hover {
|
|
|
- color: #e53e3e;
|
|
|
+.clear-completed-btn:hover {
|
|
|
+ background-color: #dc2626;
|
|
|
}
|
|
|
-</style>
|
|
|
-
|
|
|
+</style>
|