Merge branch 'dev'

This commit is contained in:
songtianlun 2025-02-01 14:45:27 +08:00
commit 9a745dcee0
18 changed files with 261 additions and 176 deletions

View File

@ -60,6 +60,8 @@ gem "aws-sdk-s3", "~> 1.177"
gem "sidekiq", "~> 7.3" gem "sidekiq", "~> 7.3"
gem "sidekiq-scheduler", "~> 5.0" gem "sidekiq-scheduler", "~> 5.0"
gem "image_processing", "~> 1.13"
group :development, :test do group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

View File

@ -164,6 +164,14 @@ GEM
multipart-post (~> 2.0) multipart-post (~> 2.0)
faraday-net_http (3.4.0) faraday-net_http (3.4.0)
net-http (>= 0.5.0) net-http (>= 0.5.0)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm-linux-gnu)
ffi (1.17.1-arm-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
formtastic (5.0.0) formtastic (5.0.0)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
formtastic_i18n (0.7.0) formtastic_i18n (0.7.0)
@ -183,6 +191,9 @@ GEM
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
i18n (1.14.6) i18n (1.14.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
inherited_resources (1.14.0) inherited_resources (1.14.0)
actionpack (>= 6.0) actionpack (>= 6.0)
has_scope (>= 0.6) has_scope (>= 0.6)
@ -240,6 +251,7 @@ GEM
matrix (0.4.2) matrix (0.4.2)
meta-tags (2.22.1) meta-tags (2.22.1)
actionpack (>= 6.0.0, < 8.1) actionpack (>= 6.0.0, < 8.1)
mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (5.25.4) minitest (5.25.4)
msgpack (1.7.5) msgpack (1.7.5)
@ -386,6 +398,9 @@ GEM
faraday (>= 1) faraday (>= 1)
faraday-multipart (>= 1) faraday-multipart (>= 1)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.2)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.4.1) rubyzip (2.4.1)
rufus-scheduler (3.9.2) rufus-scheduler (3.9.2)
@ -502,6 +517,7 @@ DEPENDENCIES
down (~> 5.4) down (~> 5.4)
friendly_id (~> 5.5) friendly_id (~> 5.5)
httparty (~> 0.22.0) httparty (~> 0.22.0)
image_processing (~> 1.13)
jbuilder jbuilder
jsbundling-rails jsbundling-rails
kamal kamal

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -1,3 +1,4 @@
@import "photoswipe/dist/photoswipe.css";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;

View File

@ -1,5 +1,6 @@
// Entry point for the build script in your package.json // Entry point for the build script in your package.json
import "@hotwired/turbo-rails" import "@hotwired/turbo-rails"
import "@hotwired/stimulus"
import "@fontsource/playfair-display/400.css"; import "@fontsource/playfair-display/400.css";
import "@fontsource/playfair-display/700.css"; import "@fontsource/playfair-display/700.css";
import "@fontsource/raleway/400.css"; import "@fontsource/raleway/400.css";

View File

@ -6,3 +6,9 @@ import { application } from "./application"
import HelloController from "./hello_controller" import HelloController from "./hello_controller"
application.register("hello", HelloController) application.register("hello", HelloController)
import PhotoSwipeLightBoxController from "./photo_swipe_lightbox_controller"
console.log("ready to register photo-swipe")
application.register("photo-swipe-lightbox", PhotoSwipeLightBoxController)
console.log("successful to register photo-swipe")

View File

@ -0,0 +1,42 @@
import { Controller } from "@hotwired/stimulus"
import PhotoSwipeLightbox from 'photoswipe/lightbox'
import PhotoSwipe from 'photoswipe'
import 'photoswipe/dist/photoswipe.css'
export default class extends Controller {
static targets = ['image', 'gallery']
connect() {
this.initPhotoSwipeLightbox()
}
initPhotoSwipeLightbox() {
const lightbox = new PhotoSwipeLightbox({
gallery: this.galleryTarget,
children: 'a',
pswpModule: PhotoSwipe,
initialZoomInEndEvent: 'mousedown',
dataSource: (items) => {
return items.map((item) => ({
src: item.dataset.pswpSrc,
w: parseInt(item.dataset.pswpWidth, 10),
h: parseInt(item.dataset.pswpHeight, 10),
title: item.dataset.pswpCaption,
}))
},
padding: { top: 0, bottom: 0, left: 0, right: 0 }, // 自定义图片与页面边界的填充
closeOnScroll: false,
zoom: true, // 启用缩放功能
bgOpacity: 0.9, // 背景透明度
pswpUIOptions: {
arrowPrev: true,
arrowNext: true,
zoom: true, // 添加缩放按钮
fullscreen: true, // 添加全屏按钮
counter: true, // 显示当前图片编号
}
})
lightbox.init()
// console.log('PhotoSwipeLightbox instance:', lightbox);
}
}

View File

@ -4,6 +4,7 @@ class WeatherArt < ApplicationRecord
belongs_to :city belongs_to :city
has_one_attached :image has_one_attached :image
has_one_attached :image_with_watermark
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
@ -50,4 +51,8 @@ class WeatherArt < ApplicationRecord
Ahoy::Event.where("properties::jsonb->>'event_type' = 'weather_art_view' AND (properties::jsonb->>'weather_art_id')::integer = ?", self.id).count Ahoy::Event.where("properties::jsonb->>'event_type' = 'weather_art_view' AND (properties::jsonb->>'weather_art_id')::integer = ?", self.id).count
end end
end end
def image_url
image.attached? ? image.blob : nil
end
end end

View File

@ -50,7 +50,7 @@ class AiService
- Temperature: #{weather_data[:temperature]}°C - Temperature: #{weather_data[:temperature]}°C
- Weather: #{weather_data[:description]} - Weather: #{weather_data[:description]}
- Cloud cover: #{weather_data[:cloud]}% - Cloud cover: #{weather_data[:cloud]}%
- Time: Early morning - Time: #{weather_data[:time]}
Requirements: Requirements:
- Feature iconic landmarks or architecture from #{city.name} - Feature iconic landmarks or architecture from #{city.name}

View File

@ -31,7 +31,8 @@ class WeatherService
pressure: data["pressure"].to_f, pressure: data["pressure"].to_f,
visibility: data["vis"].to_f, visibility: data["vis"].to_f,
cloud: data["cloud"].to_f, cloud: data["cloud"].to_f,
description: data["text"] description: data["text"],
time: response["updateTime"]
} }
end end
end end

View File

@ -33,22 +33,22 @@
<%# Includes all stylesheet files in app/assets/stylesheets %> <%# Includes all stylesheet files in app/assets/stylesheets %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %> <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<script defer data-domain="todayaiweather.com" src="https://plausible.frytea.com/js/script.js"></script> <!-- <script defer data-domain="todayaiweather.com" src="https://plausible.frytea.com/js/script.js"></script>-->
<script defer src="https://busuanzi.frytea.com/js"></script> <!-- <script defer src="https://busuanzi.frytea.com/js"></script>-->
<!-- Google tag (gtag.js) --> <!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PX1C92V5L7"></script> <!-- <script async src="https://www.googletagmanager.com/gtag/js?id=G-PX1C92V5L7"></script>-->
<script> <!-- <script>-->
window.dataLayer = window.dataLayer || []; <!-- window.dataLayer = window.dataLayer || [];-->
function gtag(){dataLayer.push(arguments);} <!-- function gtag(){dataLayer.push(arguments);}-->
gtag('js', new Date()); <!-- gtag('js', new Date());-->
gtag('config', 'G-PX1C92V5L7'); <!-- gtag('config', 'G-PX1C92V5L7');-->
</script> <!-- </script>-->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7296634171837358" <!-- <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7296634171837358"-->
crossorigin="anonymous"></script> <!-- crossorigin="anonymous"></script>-->
</head> </head>

View File

@ -12,7 +12,7 @@
<div class="join shadow-lg"> <div class="join shadow-lg">
<!-- 首页 --> <!-- 首页 -->
<%= link_to url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]), <%= link_to url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %> class: "join-item btn btn-xs #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg> </svg>
@ -20,7 +20,7 @@
<!-- 上一页 --> <!-- 上一页 -->
<%= link_to url_for(page: collection.prev_page || 1, region: params[:region], country: params[:country], sort: params[:sort]), <%= link_to url_for(page: collection.prev_page || 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %> class: "join-item btn btn-xs #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg> </svg>
@ -33,35 +33,35 @@
<% if start_page > 1 %> <% if start_page > 1 %>
<%= link_to 1, url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]), <%= link_to 1, url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-ghost hover:bg-primary/5" %> class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
<% if start_page > 2 %> <% if start_page > 2 %>
<button class="join-item btn btn-ghost btn-disabled">...</button> <button class="join-item btn btn-xs btn-ghost btn-disabled">...</button>
<% end %> <% end %>
<% end %> <% end %>
<% (start_page..end_page).each do |page| %> <% (start_page..end_page).each do |page| %>
<% if page == collection.current_page %> <% if page == collection.current_page %>
<button class="join-item btn btn-ghost bg-primary/10 font-medium"> <button class="join-item btn btn-xs btn-ghost bg-primary/10 font-medium">
<%= page %> <%= page %>
</button> </button>
<% else %> <% else %>
<%= link_to page, url_for(page: page, region: params[:region], country: params[:country], sort: params[:sort]), <%= link_to page, url_for(page: page, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-ghost hover:bg-primary/5" %> class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
<% end %> <% end %>
<% end %> <% end %>
<% if end_page < collection.total_pages %> <% if end_page < collection.total_pages %>
<% if end_page < collection.total_pages - 1 %> <% if end_page < collection.total_pages - 1 %>
<button class="join-item btn btn-ghost btn-disabled">...</button> <button class="join-item btn btn-xs btn-ghost btn-disabled">...</button>
<% end %> <% end %>
<%= link_to collection.total_pages, <%= link_to collection.total_pages,
url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]), url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-ghost hover:bg-primary/5" %> class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
<% end %> <% end %>
<!-- 下一页 --> <!-- 下一页 -->
<%= link_to url_for(page: collection.next_page || collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]), <%= link_to url_for(page: collection.next_page || collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %> class: "join-item btn btn-xs #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg> </svg>
@ -69,7 +69,7 @@
<!-- 末页 --> <!-- 末页 -->
<%= link_to url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]), <%= link_to url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %> class: "join-item btn btn-xs #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg> </svg>

View File

@ -1,11 +0,0 @@
<!-- app/views/weather_arts/_weather_stat.html.erb -->
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="<%= icon %>" />
</svg>
<div class="stat-title font-medium"><%= title %></div>
</div>
<div class="stat-value text-2xl"><%= value %></div>
<div class="stat-desc mt-1"><%= desc %></div>
</div>

View File

@ -0,0 +1,37 @@
<%# Partial _weather_stats.html.erb %>
<div class="stat bg-gradient-to-br from-primary/10 to-primary/20 hover:from-primary hover:to-primary/30 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Temperature</div>
<div class="stat-value text-3xl"><%= weather_art.temperature %>°C</div>
<div class="stat-desc">Feels like <%= weather_art.feeling_temp %>°C</div>
</div>
<div class="stat bg-gradient-to-br from-secondary/10 to-secondary/20 hover:from-secondary hover:to-secondary/30 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Wind</div>
<div class="stat-value text-3xl"><%= weather_art.wind_scale %></div>
<div class="stat-desc"><%= weather_art.wind_speed %> km/h</div>
</div>
<div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Humidity</div>
<div class="stat-value text-3xl"><%= weather_art.humidity %>%</div>
<div class="stat-desc">Relative humidity</div>
</div>
<div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Visibility</div>
<div class="stat-value text-3xl"><%= weather_art.visibility %> km</div>
<div class="stat-desc">Clear view distance</div>
</div>
<div class="stat bg-accent/10 hover:bg-accent p-4 rounded-lg">
<div class="stat-title font-medium text-base">Pressure</div>
<div class="stat-value text-3xl"><%= weather_art.pressure %> hPa</div>
<div class="stat-desc">Atmospheric pressure</div>
</div>
<div class="stat bg-base-200 hover:bg-base-100 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Cloud Cover</div>
<div class="stat-value text-3xl"><%= weather_art.cloud %>%</div>
<div class="stat-desc">Sky coverage</div>
</div>

View File

@ -4,22 +4,14 @@
</script> </script>
<% end %> <% end %>
<div class="relative min-h-screen bg-base-200"> <div class="relative min-h-screen bg-white"> <!-- 使用更明快的背景颜色 -->
<!-- 背景图片 --> <div class="container mx-auto px-4 pt-12 pb-16">
<% if @weather_art.image.attached? %> <div class="max-w-6xl mx-auto space-y-6">
<div class="fixed inset-0 -z-10">
<%= image_tag @weather_art.image,
class: "absolute w-full h-full object-cover scale-110 filter blur-2xl opacity-25" %>
<div class="absolute inset-0 bg-gradient-to-b from-base-200/90 to-base-200/70 backdrop-blur-md"></div>
</div>
<% end %>
<!-- 主要内容 -->
<div class="relative z-10">
<!-- 返回导航 --> <!-- 返回导航 -->
<div class="container mx-auto px-4 py-6"> <div class="flex items-center">
<%= link_to city_path(@weather_art.city), <%= link_to city_path(@weather_art.city),
class: "btn btn-ghost btn-lg gap-2 bg-base-100/50 backdrop-blur-sm hover:bg-base-100/70 transition-all duration-300" do %> class: "btn btn-ghost btn-md gap-2 bg-base-200 hover:bg-base-300 transition-all duration-300" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg> </svg>
@ -27,132 +19,73 @@
<% end %> <% end %>
</div> </div>
<div class="container mx-auto px-4 pb-16"> <!-- 主要内容 -->
<div class="max-w-6xl mx-auto"> <div class="card bg-base-200/80 backdrop-blur-md shadow-lg overflow-hidden"> <!-- 调整透明度和阴影 -->
<!-- 头部信息 --> <div class="grid lg:grid-cols-2 gap-6 items-center">
<div class="text-center space-y-4 mb-12">
<div class="inline-flex items-center gap-2 text-sm font-medium px-4 py-2 rounded-full bg-base-100/50 backdrop-blur-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<%= @weather_art.city.full_name %>
</div>
<h1 class="text-4xl md:text-6xl font-display font-bold"> <!-- 图片区域 -->
<span class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"> <% if @weather_art.image.attached? %>
<figure class="relative lg:h-[30rem] h-auto overflow-hidden rounded-lg"> <!-- 添加圆角 -->
<div class="gallery" data-controller="photo-swipe-lightbox">
<div data-photo-swipe-lightbox-target="gallery" class="h-full">
<% blob = @weather_art.image_blob %>
<%= link_to rails_blob_path(blob.variant(resize_to_fit: [896, 512])),
data: {
pswp_src: rails_blob_url(blob),
pswp_caption: 'Weather Art',
pswp_width: 1792,
pswp_height: 1024
} do %>
<%= image_tag @weather_art.image.variant(resize_to_fill: [896, 512]),
class: "object-cover w-full h-full transition-transform scale-95 transform hover:scale-105 ease-in-out" %> <!-- 改变缩放效果 -->
<% end %>
</div>
</div>
</figure>
<% end %>
<!-- 信息区域 -->
<div class="card-body p-8 lg:py-10 lg:px-12">
<div class="prose max-w-none">
<h1 class="font-display text-4xl md:text-5xl font-bold text-gradient mb-6">
Weather Art Weather Art
</span>
</h1> </h1>
<div class="flex flex-wrap justify-center items-center gap-3"> <div class="flex flex-wrap gap-4 mb-6">
<div class="badge badge-lg badge-primary gap-2"> <div class="badge badge-lg badge-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<%= @weather_art.weather_date.strftime("%B %d, %Y") %> <%= @weather_art.weather_date.strftime("%B %d, %Y") %>
</div> </div>
<div class="badge badge-lg badge-secondary gap-2"> <div class="badge badge-lg badge-secondary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<%= @weather_art.weather_date.strftime("%H:%M") %> <%= @weather_art.weather_date.strftime("%H:%M") %>
</div> </div>
</div> </div>
</div>
<!-- 主要卡片 --> <h2 class="text-2xl font-semibold mb-4">
<div class="card lg:card-side bg-base-100/80 backdrop-blur-md shadow-2xl">
<figure class="lg:w-1/2 relative aspect-square lg:aspect-auto group">
<% if @weather_art.image.attached? %>
<%= image_tag @weather_art.image,
class: "w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" %>
<div class="absolute inset-0 bg-gradient-to-t from-base-100/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<% end %>
</figure>
<div class="card-body lg:w-1/2">
<div class="prose max-w-none mb-8">
<h2 class="card-title font-display text-3xl mb-4 flex items-center gap-3">
<%= weather_description_icon(@weather_art.description) %> <%= weather_description_icon(@weather_art.description) %>
<%= @weather_art.description %> <%= @weather_art.description %>
</h2> </h2>
<div class="divider"></div> <div class="divider"></div>
</div>
<!-- 天气数据网格 --> <div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <%= render 'weather_stats', weather_art: @weather_art %> <!-- 使用局部渲染 -->
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("temperature") %>
<div class="stat-title font-medium">Temperature</div>
</div> </div>
<div class="stat-value text-2xl"><%= @weather_art.temperature %>°C</div>
<div class="stat-desc mt-1">Feels like <%= @weather_art.feeling_temp %>°C</div>
</div> </div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("wind") %>
<div class="stat-title font-medium">Wind</div>
</div> </div>
<div class="stat-value text-2xl"><%= @weather_art.wind_scale %></div>
<div class="stat-desc mt-1"><%= @weather_art.wind_speed %> km/h</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("humidity") %>
<div class="stat-title font-medium">Humidity</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.humidity %>%</div>
<div class="stat-desc mt-1">Relative humidity</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("visibility") %>
<div class="stat-title font-medium">Visibility</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.visibility %> km</div>
<div class="stat-desc mt-1">Clear view distance</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("pressure") %>
<div class="stat-title font-medium">Pressure</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.pressure %> hPa</div>
<div class="stat-desc mt-1">Atmospheric pressure</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("cloud") %>
<div class="stat-title font-medium">Cloud Cover</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.cloud %>%</div>
<div class="stat-desc mt-1">Sky coverage</div>
</div> </div>
</div> </div>
<!-- AI Prompt --> <!-- AI Prompt -->
<div class="mt-8"> <div class="bg-primary/10 backdrop-blur-md p-6 rounded-lg border border-primary/20">
<div class="bg-base-200/50 backdrop-blur-sm p-6 rounded-box border border-base-300">
<div class="flex items-center gap-3 mb-4"> <div class="flex items-center gap-3 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
<h3 class="font-display font-bold text-lg">AI Prompt</h3> <h3 class="font-display font-bold text-lg">AI Prompt</h3>
</div> </div>
<p class="text-base-content/70 leading-relaxed"> <p class="text-base-content/80 leading-relaxed">
<%= @weather_art.prompt %> <%= @weather_art.prompt %>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</div>
</div>
</div> </div>

View File

@ -0,0 +1,53 @@
class AddWatermarkToWeatherArtWorker
include Sidekiq::Worker
def perform(weather_art_id)
@weather_art = WeatherArt.find_by(id: weather_art_id)
return unless @weather_art
add_watermark
rescue StandardError => e
Rails.logger.error "Error adding watermark to WeatherArt #{weather_art_id}: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
end
private
attr_reader :weather_art
def add_watermark
return if weather_art.image_with_watermark.attached?
watermark_path = Rails.root.join("app/assets/images/today_ai_weather_copyright_watermark1.png")
return unless File.exist?(watermark_path)
image_tempfile = nil
watermark_tempfile = nil
begin
image_tempfile = weather_art.image.download
return unless image_dimensions_are_sufficient?(image_tempfile.path)
image = ImageProcessing::Vips.source(image_tempfile.path)
watermark = ImageProcessing::Vips.source(watermark_path)
watermarked_image = image.composite(watermark, "overlay")
watermark_tempfile = Tempfile.new([ "watermarked_image", ".png" ])
watermarked_image.write_to_file(watermark_tempfile.path)
weather_art.image_with_watermark.attach(
io: File.open(watermark_tempfile.path),
filename: "#{generate_filename("watermarked")}",
content_type: "image/png"
)
ensure
watermark_tempfile.unlink if watermark_tempfile
end
end
def image_dimensions_are_sufficient?(image_path)
dimensions = ImageProcessing::Vips.source(image_path).sizes
dimensions.width >= 200 && dimensions.height >= 200
end
def generate_filename(prefix)
"#{prefix}_#{weather_art.image.filename.base}"
end
end

View File

@ -18,6 +18,7 @@
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"jquery": "^3.7.1", "jquery": "^3.7.1",
"jquery-ui": "^1.14.1", "jquery-ui": "^1.14.1",
"photoswipe": "^5.4.4",
"postcss": "^8.5.1", "postcss": "^8.5.1",
"sass": "^1.83.4", "sass": "^1.83.4",
"tailwindcss": "^3.4.17" "tailwindcss": "^3.4.17"

View File

@ -153,7 +153,7 @@
"@hotwired/stimulus@^3.2.2": "@hotwired/stimulus@^3.2.2":
version "3.2.2" version "3.2.2"
resolved "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz" resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.2.tgz#071aab59c600fed95b97939e605ff261a4251608"
integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A== integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==
"@hotwired/turbo-rails@^8.0.12": "@hotwired/turbo-rails@^8.0.12":
@ -722,20 +722,13 @@ jiti@^1.21.6:
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz" resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz"
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
jquery-ui@^1.13.3: jquery-ui@^1.13.3, jquery-ui@^1.14.1:
version "1.14.1" version "1.14.1"
resolved "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.14.1.tgz" resolved "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.14.1.tgz"
integrity sha512-DhzsYH8VeIvOaxwi+B/2BCsFFT5EGjShdzOcm5DssWjtcpGWIMsn66rJciDA6jBruzNiLf1q0KvwMoX1uGNvnQ== integrity sha512-DhzsYH8VeIvOaxwi+B/2BCsFFT5EGjShdzOcm5DssWjtcpGWIMsn66rJciDA6jBruzNiLf1q0KvwMoX1uGNvnQ==
dependencies: dependencies:
jquery ">=1.12.0 <5.0.0" jquery ">=1.12.0 <5.0.0"
jquery-ui@^1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.14.1.tgz#ba342ea3ffff662b787595391f607d923313e040"
integrity sha512-DhzsYH8VeIvOaxwi+B/2BCsFFT5EGjShdzOcm5DssWjtcpGWIMsn66rJciDA6jBruzNiLf1q0KvwMoX1uGNvnQ==
dependencies:
jquery ">=1.12.0 <5.0.0"
jquery-ujs@^1.2.2: jquery-ujs@^1.2.2:
version "1.2.3" version "1.2.3"
resolved "https://registry.npmjs.org/jquery-ujs/-/jquery-ujs-1.2.3.tgz" resolved "https://registry.npmjs.org/jquery-ujs/-/jquery-ujs-1.2.3.tgz"
@ -853,6 +846,11 @@ path-scurry@^1.11.1:
lru-cache "^10.2.0" lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
photoswipe@^5.4.4:
version "5.4.4"
resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.4.4.tgz#e045dc036453493188d5c8665b0e8f1000ac4d6e"
integrity sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==
picocolors@^1, picocolors@^1.0.1, picocolors@^1.1.1: picocolors@^1, picocolors@^1.0.1, picocolors@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"