- Display the latest weather art on the cities index page. - Update the weather art preview image size to be big. - Improve image display by using webp format. - Adjust the daily and per-run generation limits. These changes enhance the user experience by providing more up-to-date and visually appealing content, while also controlling the resource usage during image generation.
245 lines
7.5 KiB
Ruby
245 lines
7.5 KiB
Ruby
class WeatherArt < ApplicationRecord
|
|
extend FriendlyId
|
|
friendly_id :weather_date, use: :slugged
|
|
|
|
belongs_to :city
|
|
has_one_attached :image
|
|
|
|
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
|
|
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
|
|
|
|
validates :weather_date, presence: true
|
|
validates :city_id, presence: true
|
|
|
|
scope :latest, ->(limit = 100) {
|
|
order(created_at: :desc).limit(limit)
|
|
}
|
|
|
|
scope :by_popularity, ->(limit = 100) {
|
|
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
|
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.weather_art_id') = weather_arts.id
|
|
AND json_extract(ahoy_events.properties, '$.event_type') = 'weather_art_view'")
|
|
.group("weather_arts.id")
|
|
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
|
|
.order("visit_count DESC").limit(limit)
|
|
else
|
|
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'weather_art_id')::integer = weather_arts.id
|
|
AND ahoy_events.properties::jsonb->>'event_type' = 'weather_art_view'")
|
|
.group("weather_arts.id")
|
|
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
|
|
.order("visit_count DESC").limit(limit)
|
|
end
|
|
}
|
|
|
|
scope :random, ->(limit = 3) {
|
|
if ActiveRecord::Base.connection.adapter_name.downcase == "postgresql"
|
|
# PostgreSQL 优化版本
|
|
order(Arel.sql("RANDOM()")).limit(limit)
|
|
elsif ActiveRecord::Base.connection.adapter_name.downcase == "mysql2"
|
|
# MySQL 优化版本
|
|
order(Arel.sql("RAND()")).limit(limit)
|
|
else
|
|
# SQLite 或其他数据库的通用版本
|
|
order(Arel.sql("RANDOM()")).limit(limit)
|
|
end
|
|
}
|
|
|
|
def should_generate_new_friendly_id?
|
|
weather_date_changed? || city_id_changed? || super
|
|
end
|
|
|
|
def to_s
|
|
"#{city.name} - #{weather_date.strftime('%Y-%m-%d')}"
|
|
end
|
|
|
|
def self.ransackable_associations(auth_object = nil)
|
|
[ "city", "image_attachment", "image_blob" ]
|
|
end
|
|
|
|
def self.ransackable_attributes(auth_object = nil)
|
|
[ "city_id", "cloud", "created_at", "description", "feeling_temp", "humidity", "id", "id_value", "precipitation", "pressure", "prompt", "temperature", "updated_at", "visibility", "weather_date", "wind_scale", "wind_speed" ]
|
|
end
|
|
|
|
def view_count
|
|
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
|
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'weather_art_view' AND json_extract(properties, '$.weather_art_id') = ?", self.id).count
|
|
else
|
|
Ahoy::Event.where("properties::jsonb->>'event_type' = 'weather_art_view' AND (properties::jsonb->>'weather_art_id')::integer = ?", self.id).count
|
|
end
|
|
end
|
|
|
|
def formatted_time(type = :date, use_local_timezone = false)
|
|
# 获取时区
|
|
timezone_info = self.city&.country&.timezones.present? ?
|
|
JSON.parse(self.city.country.timezones)&.first :
|
|
{ "zoneName" => "UTC", "gmtOffsetName" => "UTC+00:00" }
|
|
|
|
# 设置时区对象
|
|
time_zone = ActiveSupport::TimeZone[timezone_info["zoneName"]] ||
|
|
ActiveSupport::TimeZone["UTC"]
|
|
time = self.updated_at
|
|
|
|
date_string = self.weather_date&.strftime("%B %d, %Y")
|
|
time_string =
|
|
use_local_timezone ?
|
|
"#{time.in_time_zone(time_zone).strftime('%H:%M')} #{timezone_info['gmtOffsetName']}" :
|
|
"#{time.utc.strftime('%H:%M')} UTC"
|
|
|
|
case type
|
|
when :date
|
|
date_string
|
|
when :time
|
|
time_string
|
|
when :all
|
|
"#{date_string} #{time_string}"
|
|
else
|
|
"#{date_string} #{time_string}"
|
|
end
|
|
end
|
|
|
|
def image_url
|
|
image.attached? ? image.blob : nil
|
|
end
|
|
|
|
def webp_image
|
|
return nil unless image.attached?
|
|
|
|
image.variant(
|
|
format: "webp",
|
|
saver: {
|
|
quality: 100,
|
|
strip: true, # 移除元数据以减小文件大小
|
|
interlace: "plane" # 渐进式加载
|
|
}
|
|
)
|
|
end
|
|
|
|
# 添加图片变体处理
|
|
PREVIEW_DIMENSIONS = {
|
|
big: [ 1792, 1024 ],
|
|
medium: [ 896, 512 ],
|
|
small: [ 448, 256 ]
|
|
}.freeze
|
|
def preview_image(size = :big)
|
|
return nil unless image.attached?
|
|
|
|
width, height = PREVIEW_DIMENSIONS[size] || PREVIEW_DIMENSIONS[:medium]
|
|
|
|
image.variant(
|
|
resize_to_limit: [ width, height ],
|
|
format: "webp",
|
|
saver: {
|
|
quality: 75,
|
|
strip: true, # 移除元数据以减小文件大小
|
|
interlace: "plane" # 渐进式加载
|
|
}
|
|
)
|
|
end
|
|
|
|
def watermarked_image
|
|
return nil unless image.attached?
|
|
|
|
overlay_text = create_overlay_text
|
|
|
|
image.variant(
|
|
composite: [ {
|
|
input: overlay_text,
|
|
gravity: "southeast"
|
|
} ]
|
|
)
|
|
end
|
|
|
|
private
|
|
|
|
def create_overlay_text
|
|
{
|
|
create: {
|
|
width: 400,
|
|
height: 100,
|
|
background: [ 0, 0, 0, 0.5 ] # 半透明黑色背景
|
|
},
|
|
"svg-overlay": %(
|
|
<svg width="400" height="100">
|
|
<text x="20" y="40"
|
|
style="fill: white; font-family: Arial; font-size: 20px;">
|
|
#{city.name} - #{weather_date.strftime('%Y-%m-%d')}
|
|
</text>
|
|
<text x="20" y="70"
|
|
style="fill: white; font-family: Arial; font-size: 20px;">
|
|
© todayaiweather.com
|
|
</text>
|
|
</svg>
|
|
)
|
|
}
|
|
end
|
|
|
|
def create_text_layer(font_size)
|
|
text = [
|
|
weather_date.strftime("%Y-%m-%d"),
|
|
"#{temperature}°C, #{description}",
|
|
"#{city.name}, #{city.country.name}, #{city.country.region.name}",
|
|
"© todayaiweather.com"
|
|
].join("\n")
|
|
|
|
{
|
|
create: {
|
|
width: 600,
|
|
height: 200,
|
|
background: [ 0, 0, 0, 0 ]
|
|
},
|
|
"svg-overlay": %(
|
|
<svg width="600" height="200">
|
|
<style>
|
|
.text {
|
|
font-family: Arial, sans-serif;
|
|
font-size: #{font_size}px;
|
|
}
|
|
.shadow {
|
|
fill: white;
|
|
stroke: black;
|
|
stroke-width: 2px;
|
|
paint-order: stroke fill;
|
|
}
|
|
</style>
|
|
<text x="20" y="#{font_size + 10}" class="text shadow">#{weather_date.strftime('%Y-%m-%d')}</text>
|
|
<text x="20" y="#{font_size * 2 + 20}" class="text shadow">#{temperature}°C, #{description}</text>
|
|
<text x="20" y="#{font_size * 3 + 30}" class="text shadow">#{city.name}, #{city.country.name}</text>
|
|
<text x="20" y="#{font_size * 4 + 40}" class="text shadow">© todayaiweather.com</text>
|
|
</svg>
|
|
)
|
|
}
|
|
end
|
|
|
|
def watermark_command(font_size:, stroke_width:, spacing:)
|
|
date_str = weather_date.strftime("%Y-%m-%d")
|
|
weather_info = "#{temperature}°C, #{description}"
|
|
location_info = "#{city.name}, #{city.country.name}, #{city.country.region.name}"
|
|
copyright = "© todayaiweather.com"
|
|
|
|
"gravity southeast " \
|
|
"fill white " \
|
|
"font Arial " \
|
|
"pointsize #{font_size} " \
|
|
"stroke black " \
|
|
"strokewidth #{stroke_width} " \
|
|
"text 30,#{spacing * 12} '#{copyright}' " \
|
|
"text 30,#{spacing * 8} '#{location_info}' " \
|
|
"text 30,#{spacing * 4} '#{weather_info}' " \
|
|
"text 30,#{spacing} '#{date_str}'"
|
|
end
|
|
|
|
def watermark_text
|
|
date_str = weather_date.strftime("%Y-%m-%d")
|
|
weather_info = "#{temperature}°C, #{description}"
|
|
location_info = "#{city.name}, #{city.country.name}, #{city.country.region.name}"
|
|
copyright = "© todayaiweather.com"
|
|
|
|
[
|
|
"text 30,120 '#{copyright}'",
|
|
"text 30,80 '#{location_info}'",
|
|
"text 30,40 '#{weather_info}'",
|
|
"text 30,0 '#{date_str}'"
|
|
].join(" ")
|
|
end
|
|
end
|