feat: add cities feature with weather arts
- Implement CitiesController for listing and showing cities - Create City and WeatherArt models with associations - Add views for cities index and show, displaying weather arts - Include routes for cities and active storage for images - Update migrations for weather arts and seed data for testing This commit introduces a comprehensive cities feature that allows users to view cities along with their associated weather art. The implementation includes necessary database migrations, routes, and controller actions to support this new functionality.
This commit is contained in:
parent
f4857f73fc
commit
8e8c60254f
11
app/controllers/cities_controller.rb
Normal file
11
app/controllers/cities_controller.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class CitiesController < ApplicationController
|
||||
def index
|
||||
@cities = City.all
|
||||
@cities = City.all.includes(:weather_arts).page(params[:page])
|
||||
end
|
||||
|
||||
def show
|
||||
@city = City.find(params[:id])
|
||||
@weather_arts = @city.weather_arts.latest.page(params[:page])
|
||||
end
|
||||
end
|
@ -1,5 +1,7 @@
|
||||
class StaticPagesController < ApplicationController
|
||||
def home
|
||||
@featured_cities = City.featured.limit(5)
|
||||
@latest_arts = WeatherArt.latest.limit(6)
|
||||
end
|
||||
|
||||
def help
|
||||
|
2
app/helpers/cities_helper.rb
Normal file
2
app/helpers/cities_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module CitiesHelper
|
||||
end
|
@ -1,2 +1,13 @@
|
||||
class City < ApplicationRecord
|
||||
has_many :weather_arts
|
||||
|
||||
scope :featured, -> { where(featured: true) }
|
||||
|
||||
def current_weather_art
|
||||
weather_arts.find_by(weather_date: Date.current)
|
||||
end
|
||||
|
||||
def weather_art_for_date(date)
|
||||
weather_arts.find_by(weather_date: date)
|
||||
end
|
||||
end
|
||||
|
@ -1,2 +1,12 @@
|
||||
class WeatherArt < ApplicationRecord
|
||||
belongs_to :city
|
||||
|
||||
has_one_attached :image
|
||||
|
||||
scope :latest, -> { order(created_at: :desc) }
|
||||
|
||||
def image_url
|
||||
# 这里实现获取图片URL的逻辑,可以是AWS S3或其他存储服务
|
||||
Rails.application.routes.url_helpers.rails_blob_path(image, only_path: true) if image.attached?
|
||||
end
|
||||
end
|
||||
|
20
app/views/cities/index.html.erb
Normal file
20
app/views/cities/index.html.erb
Normal file
@ -0,0 +1,20 @@
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold mb-8">Cities</h1>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<% @cities.each do |city| %>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title"><%= city.name %></h2>
|
||||
<p><%= city.country %></p>
|
||||
<div class="card-actions justify-end">
|
||||
<%= link_to 'View Weather History', city_path(city),
|
||||
class: "btn btn-primary" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= paginate @cities %>
|
||||
</div>
|
130
app/views/cities/show.html.erb
Normal file
130
app/views/cities/show.html.erb
Normal file
@ -0,0 +1,130 @@
|
||||
<!-- app/views/cities/show.html.erb -->
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- 城市信息头部 -->
|
||||
<div class="bg-base-200 rounded-box p-8 mb-8">
|
||||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold mb-2"><%= @city.name %></h1>
|
||||
<div class="flex items-center gap-2 text-base-content/70">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span><%= @city.country %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前天气信息 -->
|
||||
<% if @current_weather_art = @city.current_weather_art %>
|
||||
<div class="stats shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Temperature</div>
|
||||
<div class="stat-value"><%= @current_weather_art.temperature %>°C</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Condition</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日期筛选器 -->
|
||||
<div class="flex flex-col md:flex-row gap-4 items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="text-2xl font-bold">Weather Art History</h2>
|
||||
<div class="badge badge-primary"><%= @weather_arts.total_count %> artworks</div>
|
||||
</div>
|
||||
|
||||
<%= form_tag city_path(@city), method: :get, class: "join" do %>
|
||||
<%= date_field_tag :start_date, params[:start_date],
|
||||
class: "join-item input input-bordered w-full max-w-xs",
|
||||
placeholder: "Start Date" %>
|
||||
<%= date_field_tag :end_date, params[:end_date],
|
||||
class: "join-item input input-bordered w-full max-w-xs",
|
||||
placeholder: "End Date" %>
|
||||
<%= submit_tag "Filter", class: "join-item btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- 天气艺术网格 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
||||
<% @weather_arts.each do |art| %>
|
||||
<div class="card bg-base-100 shadow-xl group hover:scale-105 transition-transform duration-200"
|
||||
data-controller="modal"
|
||||
data-action="click->modal#open">
|
||||
<!-- 卡片主体 -->
|
||||
<figure class="relative">
|
||||
<%= image_tag art.image_url,
|
||||
class: "w-full h-64 object-cover" %>
|
||||
<div class="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/70 to-transparent text-white">
|
||||
<div class="text-lg font-bold"><%= art.weather_date.strftime("%B %d, %Y") %></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span><%= art.temperature %>°C</span>
|
||||
<span class="text-xs">|</span>
|
||||
<span><%= art.description %></span>
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<!-- 模态框内容 -->
|
||||
<dialog id="modal_<%= art.id %>" class="modal">
|
||||
<div class="modal-box max-w-4xl">
|
||||
<h3 class="font-bold text-lg mb-4">
|
||||
Weather Art - <%= art.weather_date.strftime("%B %d, %Y") %>
|
||||
</h3>
|
||||
<%= image_tag art.image_url, class: "w-full rounded-lg mb-4" %>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
<div class="stat bg-base-200 rounded-box">
|
||||
<div class="stat-title">Temperature</div>
|
||||
<div class="stat-value text-lg"><%= art.temperature %>°C</div>
|
||||
</div>
|
||||
<div class="stat bg-base-200 rounded-box">
|
||||
<div class="stat-title">Humidity</div>
|
||||
<div class="stat-value text-lg"><%= art.humidity %>%</div>
|
||||
</div>
|
||||
<div class="stat bg-base-200 rounded-box">
|
||||
<div class="stat-title">Wind Speed</div>
|
||||
<div class="stat-value text-lg"><%= art.wind_speed %></div>
|
||||
</div>
|
||||
<div class="stat bg-base-200 rounded-box">
|
||||
<div class="stat-title">Condition</div>
|
||||
<div class="stat-value text-lg"><%= art.description %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-base-200 p-4 rounded-box mb-4">
|
||||
<h4 class="font-bold mb-2">AI Prompt</h4>
|
||||
<p class="text-sm"><%= art.prompt %></p>
|
||||
</div>
|
||||
<div class="modal-action">
|
||||
<form method="dialog">
|
||||
<button class="btn">Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="flex justify-center">
|
||||
<%= paginate @weather_arts %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stimulus Controller for Modal -->
|
||||
<%# app/javascript/controllers/modal_controller.js %>
|
||||
<script type="module">
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
open(event) {
|
||||
const modalId = this.element.querySelector('dialog').id
|
||||
const modal = document.getElementById(modalId)
|
||||
modal.showModal()
|
||||
}
|
||||
}
|
||||
</script>
|
@ -14,6 +14,8 @@
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
<li><%= link_to "Home", root_url %></li>
|
||||
<li><%= link_to "Help", help_url %></li>
|
||||
<li><%= link_to 'Calendar', "#" %></li>
|
||||
<li><%= link_to 'Cities', "#" %></li>
|
||||
<% if logged_in? %>
|
||||
<li><%= link_to "Users", users_path %></li>
|
||||
<li>
|
||||
@ -56,6 +58,8 @@
|
||||
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow">
|
||||
<li><%= link_to "Home", root_url %></li>
|
||||
<li><%= link_to "Help", help_url %></li>
|
||||
<li><%= link_to 'Cities', "#" %></li>
|
||||
<li><%= link_to 'Calendar', "#" %></li>
|
||||
<% if logged_in? %>
|
||||
<li><%= link_to "Users", users_path %></li>
|
||||
<li class="dropdown">
|
||||
|
@ -1,15 +1,17 @@
|
||||
<% provide(:title, "Home") %>
|
||||
<div class="hero bg-base-200 min-h-screen">
|
||||
<div class="hero-content text-center">
|
||||
<div class="hero min-h-screen" >
|
||||
<div class="hero-overlay bg-opacity-60"></div>
|
||||
<div class="hero-content text-center text-neutral-content">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold">Welcome to the Today Ai Weather</h1>
|
||||
<p class="py-6">
|
||||
This is the home page for the
|
||||
Today Ai Weather application.
|
||||
</p>
|
||||
<%= link_to "Sing up now!", signup_path, class:"btn btn-lg btn-primary" %>
|
||||
<h1 class="mb-5 text-5xl font-bold">AI Weather Art</h1>
|
||||
<p class="mb-5">Experience weather through AI-generated artwork for cities around the world.</p>
|
||||
<div class="flex gap-4 justify-center">
|
||||
<%= link_to 'Explore by Calendar', "#", class: "btn btn-primary" %>
|
||||
<%= link_to 'Browse Cities', cities_path, class: "btn btn-secondary" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<%#= link_to image_tag("kitten.jpg", alt:"Kitten", width:"200") %>
|
||||
|
@ -23,6 +23,10 @@ Rails.application.routes.draw do
|
||||
|
||||
get "pages/home"
|
||||
|
||||
resources "cities"
|
||||
|
||||
get 'calendar', to: 'calendar#index'
|
||||
|
||||
resources :users
|
||||
resources :account_activations, only: [ :edit ]
|
||||
resources :password_resets, only: [ :new, :create, :edit, :update ]
|
||||
|
@ -1,9 +1,18 @@
|
||||
class CreateWeatherArts < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :weather_arts do |t|
|
||||
t.date :weather_date
|
||||
t.string :weather_condition
|
||||
t.text :description
|
||||
t.references :city, null: false, foreign_key: true
|
||||
t.date :weather_date, null: false
|
||||
t.string :description
|
||||
t.integer :temperature
|
||||
t.integer :feeling_temp
|
||||
t.integer :humidity
|
||||
t.integer :wind_scale
|
||||
t.integer :wind_speed
|
||||
t.decimal :precipitation
|
||||
t.integer :pressure
|
||||
t.integer :visibility
|
||||
t.integer :cloud
|
||||
t.string :prompt
|
||||
|
||||
t.timestamps
|
||||
|
@ -0,0 +1,57 @@
|
||||
# This migration comes from active_storage (originally 20170806125915)
|
||||
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
# Use Active Record's configured type for primary and foreign keys
|
||||
primary_key_type, foreign_key_type = primary_and_foreign_key_types
|
||||
|
||||
create_table :active_storage_blobs, id: primary_key_type do |t|
|
||||
t.string :key, null: false
|
||||
t.string :filename, null: false
|
||||
t.string :content_type
|
||||
t.text :metadata
|
||||
t.string :service_name, null: false
|
||||
t.bigint :byte_size, null: false
|
||||
t.string :checksum
|
||||
|
||||
if connection.supports_datetime_with_precision?
|
||||
t.datetime :created_at, precision: 6, null: false
|
||||
else
|
||||
t.datetime :created_at, null: false
|
||||
end
|
||||
|
||||
t.index [ :key ], unique: true
|
||||
end
|
||||
|
||||
create_table :active_storage_attachments, id: primary_key_type do |t|
|
||||
t.string :name, null: false
|
||||
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
|
||||
t.references :blob, null: false, type: foreign_key_type
|
||||
|
||||
if connection.supports_datetime_with_precision?
|
||||
t.datetime :created_at, precision: 6, null: false
|
||||
else
|
||||
t.datetime :created_at, null: false
|
||||
end
|
||||
|
||||
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
|
||||
t.foreign_key :active_storage_blobs, column: :blob_id
|
||||
end
|
||||
|
||||
create_table :active_storage_variant_records, id: primary_key_type do |t|
|
||||
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
|
||||
t.string :variation_digest, null: false
|
||||
|
||||
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
|
||||
t.foreign_key :active_storage_blobs, column: :blob_id
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def primary_and_foreign_key_types
|
||||
config = Rails.configuration.generators
|
||||
setting = config.options[config.orm][:primary_key_type]
|
||||
primary_key_type = setting || :primary_key
|
||||
foreign_key_type = setting || :bigint
|
||||
[ primary_key_type, foreign_key_type ]
|
||||
end
|
||||
end
|
50
db/schema.rb
generated
50
db/schema.rb
generated
@ -10,7 +10,35 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_01_17_095400) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_01_18_071658) do
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
t.bigint "record_id", null: false
|
||||
t.bigint "blob_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
|
||||
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_blobs", force: :cascade do |t|
|
||||
t.string "key", null: false
|
||||
t.string "filename", null: false
|
||||
t.string "content_type"
|
||||
t.text "metadata"
|
||||
t.string "service_name", null: false
|
||||
t.bigint "byte_size", null: false
|
||||
t.string "checksum"
|
||||
t.datetime "created_at", null: false
|
||||
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
||||
end
|
||||
|
||||
create_table "active_storage_variant_records", force: :cascade do |t|
|
||||
t.bigint "blob_id", null: false
|
||||
t.string "variation_digest", null: false
|
||||
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "cities", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.string "country"
|
||||
@ -38,11 +66,25 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_17_095400) do
|
||||
end
|
||||
|
||||
create_table "weather_arts", force: :cascade do |t|
|
||||
t.date "weather_date"
|
||||
t.string "weather_condition"
|
||||
t.text "description"
|
||||
t.integer "city_id", null: false
|
||||
t.date "weather_date", null: false
|
||||
t.string "description"
|
||||
t.integer "temperature"
|
||||
t.integer "feeling_temp"
|
||||
t.integer "humidity"
|
||||
t.integer "wind_scale"
|
||||
t.integer "wind_speed"
|
||||
t.decimal "precipitation"
|
||||
t.integer "pressure"
|
||||
t.integer "visibility"
|
||||
t.integer "cloud"
|
||||
t.string "prompt"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["city_id"], name: "index_weather_arts_on_city_id"
|
||||
end
|
||||
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "weather_arts", "cities"
|
||||
end
|
||||
|
32
db/seeds.rb
32
db/seeds.rb
@ -16,7 +16,7 @@ User.create!(name: "Example User",
|
||||
activated: true,
|
||||
activated_at: Time.zone.now)
|
||||
|
||||
99.times do |n|
|
||||
9.times do |n|
|
||||
name = Faker::Name.name
|
||||
email = "example-#{n+1}@railstutorial.org"
|
||||
password = "password"
|
||||
@ -27,3 +27,33 @@ User.create!(name: "Example User",
|
||||
activated: true,
|
||||
activated_at: Time.zone.now)
|
||||
end
|
||||
|
||||
City.create!(
|
||||
name: "Guangzhou",
|
||||
country: "China",
|
||||
latitude: 23.125178,
|
||||
longitude: 113.280637,
|
||||
featured: true,
|
||||
)
|
||||
|
||||
sample_art = WeatherArt.create!(
|
||||
city_id: City.first.id,
|
||||
weather_date: Time.zone.today,
|
||||
description: "Sunny",
|
||||
temperature: 21,
|
||||
feeling_temp: 19,
|
||||
humidity: 28,
|
||||
wind_scale: 2,
|
||||
wind_speed: 8,
|
||||
precipitation: 0.0,
|
||||
pressure: 1008,
|
||||
visibility: 30,
|
||||
cloud: 0,
|
||||
prompt: "Artistic sunny weather in Guangzhou"
|
||||
)
|
||||
|
||||
sample_art.image.attach(
|
||||
io: File.open("db/seeds/images/sample_guangzhou_weather_art.png"),
|
||||
filename: "sample_guangzhou_art.png",
|
||||
content_type: "image/png"
|
||||
)
|
||||
|
BIN
db/seeds/images/sample_guangzhou_weather_art.png
Normal file
BIN
db/seeds/images/sample_guangzhou_weather_art.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
7
test/controllers/cities_controller_test.rb
Normal file
7
test/controllers/cities_controller_test.rb
Normal file
@ -0,0 +1,7 @@
|
||||
require "test_helper"
|
||||
|
||||
class CitiesControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
Loading…
Reference in New Issue
Block a user