ad_space 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +240 -0
- data/MIT-LICENSE +20 -0
- data/README.md +92 -0
- data/Rakefile +8 -0
- data/ad_space.gemspec +25 -0
- data/app/assets/builds/application.js +7397 -0
- data/app/assets/builds/application.js.map +7 -0
- data/app/assets/config/ad_space_manifest.js +2 -0
- data/app/assets/javascript/ad_space/application.js +0 -0
- data/app/assets/stylesheets/ad_space/application.css +15 -0
- data/app/controllers/ad_space/advertisements_controller.rb +121 -0
- data/app/controllers/ad_space/api/v1/advertisements_controller.rb +36 -0
- data/app/controllers/ad_space/application_controller.rb +30 -0
- data/app/controllers/ad_space/dashboard_controller.rb +6 -0
- data/app/controllers/ad_space/slots_controller.rb +70 -0
- data/app/helpers/ad_space/advertisements_helper.rb +4 -0
- data/app/helpers/ad_space/application_helper.rb +15 -0
- data/app/helpers/ad_space/dashboard_helper.rb +4 -0
- data/app/helpers/ad_space/slots_helper.rb +4 -0
- data/app/javascript/application.js +3 -0
- data/app/javascript/controllers/application.js +9 -0
- data/app/javascript/controllers/hello_controller.js +153 -0
- data/app/javascript/controllers/image_upload_controller.js +46 -0
- data/app/javascript/controllers/index.js +11 -0
- data/app/jobs/ad_space/application_job.rb +4 -0
- data/app/mailers/ad_space/application_mailer.rb +6 -0
- data/app/models/ad_space/advertisement.rb +67 -0
- data/app/models/ad_space/application_record.rb +5 -0
- data/app/models/ad_space/breadcrumb.rb +14 -0
- data/app/models/ad_space/slot.rb +27 -0
- data/app/serializers/v1/advertisement_serializer.rb +30 -0
- data/app/validators/url_or_custom_scheme_validator.rb +20 -0
- data/app/views/ad_space/advertisements/_advertisement.html.erb +102 -0
- data/app/views/ad_space/advertisements/_form.html.erb +143 -0
- data/app/views/ad_space/advertisements/edit.html.erb +10 -0
- data/app/views/ad_space/advertisements/index.html.erb +15 -0
- data/app/views/ad_space/advertisements/new.html.erb +9 -0
- data/app/views/ad_space/advertisements/show.html.erb +13 -0
- data/app/views/ad_space/dashboard/index.html.erb +7 -0
- data/app/views/ad_space/slots/_form.html.erb +44 -0
- data/app/views/ad_space/slots/_slot.html.erb +30 -0
- data/app/views/ad_space/slots/edit.html.erb +10 -0
- data/app/views/ad_space/slots/index.html.erb +14 -0
- data/app/views/ad_space/slots/new.html.erb +9 -0
- data/app/views/ad_space/slots/show.html.erb +10 -0
- data/app/views/layouts/ad_space/application.html.erb +43 -0
- data/config/routes.rb +22 -0
- data/db/migrate/20231026152912_create_ad_space_slots.rb +11 -0
- data/db/migrate/20231026160758_create_ad_space_advertisements.rb +25 -0
- data/db/migrate/20231027070536_add_scheme_url_to_ad_space_advertisements.rb +10 -0
- data/db/migrate/20231027083935_add_target_version_code_to_ad_space_advertisements.rb +6 -0
- data/db/migrate/20231027115707_add_is_splash_to_ad_space_slots.rb +5 -0
- data/lib/ad_space/app.rb +16 -0
- data/lib/ad_space/configuration.rb +32 -0
- data/lib/ad_space/engine.rb +12 -0
- data/lib/ad_space/version.rb +3 -0
- data/lib/ad_space.rb +17 -0
- data/lib/tasks/ad_space_tasks.rake +4 -0
- metadata +161 -0
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module AdSpace
|
2
|
+
class AdvertisementsController < ApplicationController
|
3
|
+
before_action :set_advertisement, only: %i[ show edit update destroy remove_cover publish archive]
|
4
|
+
|
5
|
+
# GET /advertisements
|
6
|
+
def index
|
7
|
+
@advertisements = Advertisement.all
|
8
|
+
|
9
|
+
add_breadcrumb("Home", root_path)
|
10
|
+
end
|
11
|
+
|
12
|
+
# GET /advertisements/1
|
13
|
+
def show
|
14
|
+
add_breadcrumb("Home", root_path)
|
15
|
+
add_breadcrumb("Advertisements", advertisements_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
# GET /advertisements/new
|
19
|
+
def new
|
20
|
+
@advertisement = Advertisement.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# GET /advertisements/1/edit
|
24
|
+
def edit
|
25
|
+
end
|
26
|
+
|
27
|
+
# POST /advertisements
|
28
|
+
def create
|
29
|
+
create_params = advertisement_params.to_hash.deep_symbolize_keys
|
30
|
+
create_params[:target_version_codes] = create_params[:target_version_codes].split(',').map(&:strip).map(&:to_i).uniq.compact
|
31
|
+
@advertisement = Advertisement.new(create_params)
|
32
|
+
|
33
|
+
if @advertisement.save
|
34
|
+
redirect_to @advertisement, notice: "Advertisement was successfully created."
|
35
|
+
else
|
36
|
+
render :new, status: :unprocessable_entity
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# PATCH/PUT /advertisements/1
|
41
|
+
def update
|
42
|
+
update_params = advertisement_params.to_hash.deep_symbolize_keys
|
43
|
+
update_params[:target_version_codes] = update_params[:target_version_codes].split(',').map(&:strip).map(&:to_i).uniq.compact
|
44
|
+
|
45
|
+
if @advertisement.update(update_params)
|
46
|
+
redirect_to @advertisement, notice: "Advertisement was successfully updated.", status: :see_other
|
47
|
+
else
|
48
|
+
render :edit, status: :unprocessable_entity
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# DELETE /advertisements/1
|
53
|
+
def destroy
|
54
|
+
@advertisement.destroy!
|
55
|
+
redirect_to advertisements_url, notice: "Advertisement was successfully destroyed.", status: :see_other
|
56
|
+
end
|
57
|
+
|
58
|
+
def remove_cover
|
59
|
+
size = params[:size]
|
60
|
+
|
61
|
+
# 根据传递的参数来决定要删除哪个尺寸的封面图片
|
62
|
+
case size
|
63
|
+
when "sm"
|
64
|
+
@advertisement.cover_sm.purge if @advertisement.cover_sm.attached?
|
65
|
+
when "base"
|
66
|
+
@advertisement.cover_base.purge if @advertisement.cover_base.attached?
|
67
|
+
when "md"
|
68
|
+
@advertisement.cover_md.purge if @advertisement.cover_md.attached?
|
69
|
+
when "lg"
|
70
|
+
@advertisement.cover_lg.purge if @advertisement.cover_lg.attached?
|
71
|
+
else
|
72
|
+
render json: { error: "Invalid size parameter" }, status: :unprocessable_entity
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
render json: { success: true }
|
77
|
+
end
|
78
|
+
|
79
|
+
def publish
|
80
|
+
unless @advertisement.published?
|
81
|
+
@advertisement.update(published_at: Time.zone.now)
|
82
|
+
else
|
83
|
+
@advertisement.update(published_at: nil)
|
84
|
+
end
|
85
|
+
|
86
|
+
render turbo_stream: turbo_stream.update(
|
87
|
+
view_context.dom_id(@advertisement),
|
88
|
+
partial: "advertisement",
|
89
|
+
locals: { advertisement: @advertisement }
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
def archive
|
94
|
+
unless @advertisement.archived?
|
95
|
+
@advertisement.update(archived_at: Time.zone.now)
|
96
|
+
else
|
97
|
+
@advertisement.update(archived_at: nil)
|
98
|
+
end
|
99
|
+
|
100
|
+
render turbo_stream: turbo_stream.update(
|
101
|
+
view_context.dom_id(@advertisement),
|
102
|
+
partial: "advertisement",
|
103
|
+
locals: { advertisement: @advertisement }
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
# Use callbacks to share common setup or constraints between actions.
|
109
|
+
def set_advertisement
|
110
|
+
@advertisement = Advertisement.find(params[:id])
|
111
|
+
end
|
112
|
+
|
113
|
+
# Only allow a list of trusted parameters through.
|
114
|
+
def advertisement_params
|
115
|
+
params.require(:advertisement).permit(:name, :uuid, :slot_id, :published_at,
|
116
|
+
:started_at, :ended_at, :platform, :archived_at,
|
117
|
+
:weight, :remark, :cover_sm, :cover_base, :cover_md, :cover_lg,
|
118
|
+
:target_version_codes, :ad_type, :visibility, :scheme_url, :splash_ui_type, :splash_countdown, :dsp_type)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class AdSpace::Api::V1::AdvertisementsController < ActionController::API
|
2
|
+
# before_action :prepare_params!, only: %w[index]
|
3
|
+
|
4
|
+
def index
|
5
|
+
width = params[:screen_width].to_f
|
6
|
+
height = params[:screen_height].to_f
|
7
|
+
screen_ratio = width.zero? ? 0 : height / width
|
8
|
+
size = if screen_ratio < 1.7
|
9
|
+
"little"
|
10
|
+
elsif screen_ratio >= 1.7 && screen_ratio < 2.0
|
11
|
+
"middle"
|
12
|
+
else
|
13
|
+
"large"
|
14
|
+
end
|
15
|
+
|
16
|
+
builder = AdSpace::Advertisement.includes(:slot).where(platform: params[:platform])
|
17
|
+
builder = builder.published.unexpired
|
18
|
+
builder = builder.where(slot_id: params[:slot_ids].split(",")) unless params[:slot_ids].blank?
|
19
|
+
|
20
|
+
# TODO: 动态可配置,不同项目可以自定义特殊的查询条件,基于builder
|
21
|
+
# if current_user && current_user&.is_member?
|
22
|
+
# builder = builder.where.not(position_code: '10003')
|
23
|
+
# end
|
24
|
+
|
25
|
+
unless params[:version_code].blank?
|
26
|
+
array = [0]
|
27
|
+
array.push(params[:version_code].to_i)
|
28
|
+
codes = PG::TextEncoder::Array.new.encode(array.presence || [0]).force_encoding("utf-8")
|
29
|
+
builder = builder.where("target_version_codes && ?", codes)
|
30
|
+
end
|
31
|
+
|
32
|
+
advertisements = builder.order(weight: :desc)
|
33
|
+
|
34
|
+
render json: advertisements, each_serializer: V1::AdvertisementSerializer, size: size
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module AdSpace
|
2
|
+
class ApplicationController < ActionController::Base
|
3
|
+
helper_method :_current_user
|
4
|
+
|
5
|
+
before_action :init_app
|
6
|
+
|
7
|
+
def init_app
|
8
|
+
AdSpace::App.init request: request
|
9
|
+
|
10
|
+
# 检查权限
|
11
|
+
if !_current_user&.admin?
|
12
|
+
redirect_to main_app.root_path, alert: "Permission denied - You are not authorized to access this resource.", status: :see_other
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def _current_user
|
17
|
+
@user ||= User.find_by(id: session[:user_id])
|
18
|
+
end
|
19
|
+
|
20
|
+
helper_method :breadcrumbs
|
21
|
+
|
22
|
+
def breadcrumbs
|
23
|
+
@breadcrumbs ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_breadcrumb(name, path = nil)
|
27
|
+
breadcrumbs << AdSpace::Breadcrumb.new(name, path)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module AdSpace
|
2
|
+
class SlotsController < ApplicationController
|
3
|
+
before_action :set_slot, only: %i[ show edit update destroy remove_demo]
|
4
|
+
|
5
|
+
# GET /slots
|
6
|
+
def index
|
7
|
+
@slots = Slot.all
|
8
|
+
|
9
|
+
add_breadcrumb("Home", root_path)
|
10
|
+
end
|
11
|
+
|
12
|
+
# GET /slots/1
|
13
|
+
def show
|
14
|
+
add_breadcrumb("Home", root_path)
|
15
|
+
add_breadcrumb("Advertisements", advertisements_path)
|
16
|
+
end
|
17
|
+
|
18
|
+
# GET /slots/new
|
19
|
+
def new
|
20
|
+
@slot = Slot.new
|
21
|
+
end
|
22
|
+
|
23
|
+
# GET /slots/1/edit
|
24
|
+
def edit
|
25
|
+
end
|
26
|
+
|
27
|
+
# POST /slots
|
28
|
+
def create
|
29
|
+
@slot = Slot.new(slot_params)
|
30
|
+
|
31
|
+
if @slot.save
|
32
|
+
redirect_to @slot, notice: "Slot was successfully created."
|
33
|
+
else
|
34
|
+
render :new, status: :unprocessable_entity
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# PATCH/PUT /slots/1
|
39
|
+
def update
|
40
|
+
if @slot.update(slot_params)
|
41
|
+
redirect_to @slot, notice: "Slot was successfully updated.", status: :see_other
|
42
|
+
else
|
43
|
+
render :edit, status: :unprocessable_entity
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# DELETE /slots/1
|
48
|
+
def destroy
|
49
|
+
@slot.destroy!
|
50
|
+
redirect_to slots_url, notice: "Slot was successfully destroyed.", status: :see_other
|
51
|
+
end
|
52
|
+
|
53
|
+
def remove_demo
|
54
|
+
@slot.demo.purge if @slot.demo.attached?
|
55
|
+
|
56
|
+
render json: { success: true }
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
# Use callbacks to share common setup or constraints between actions.
|
61
|
+
def set_slot
|
62
|
+
@slot = Slot.find(params[:id])
|
63
|
+
end
|
64
|
+
|
65
|
+
# Only allow a list of trusted parameters through.
|
66
|
+
def slot_params
|
67
|
+
params.require(:slot).permit(:name, :remark, :demo, :is_splash)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module AdSpace
|
2
|
+
module ApplicationHelper
|
3
|
+
def user_identifier(user)
|
4
|
+
if user.respond_to?(:email) && user.email.present?
|
5
|
+
user.email
|
6
|
+
elsif user.respond_to?(:username) && user.username.present?
|
7
|
+
user.username.camelize
|
8
|
+
elsif user.respond_to?(:name) && user.name.present?
|
9
|
+
user.name.camelize
|
10
|
+
else
|
11
|
+
user.id
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
let isShowSplashInputs = false;
|
4
|
+
|
5
|
+
export default class extends Controller {
|
6
|
+
// 默认是dsp广告
|
7
|
+
static values = {
|
8
|
+
isDspAd: {type: Boolean, default: true},
|
9
|
+
isSplash: {type: Boolean, default: false}
|
10
|
+
}
|
11
|
+
static targets = [ "adType", "slot", "splashUiType", "splashCountdown", "smCover", "mdCover", "lgCover", "baseCover", "schemeUrl" ]
|
12
|
+
|
13
|
+
connect() {
|
14
|
+
this.render()
|
15
|
+
}
|
16
|
+
|
17
|
+
handleAdTypeChnage(event) {
|
18
|
+
this.isDspAdValue = event.target.value == 'dsp'
|
19
|
+
this.render()
|
20
|
+
}
|
21
|
+
|
22
|
+
handleSlotChange(event) {
|
23
|
+
const isSplash = event.target[event.target.selectedIndex].getAttribute('data-is-splash')
|
24
|
+
this.isSplashValue = isSplash === "true"
|
25
|
+
this.render()
|
26
|
+
}
|
27
|
+
|
28
|
+
render() {
|
29
|
+
if (this.isSplashValue) {
|
30
|
+
if (this.isDspAdValue) {
|
31
|
+
// 开屏且为dsp类广告,隐藏除倒计时、ui类型之外的参数
|
32
|
+
this.schemeUrlTarget.style.display = "none";
|
33
|
+
const schemeUrlElement = this.schemeUrlTarget.querySelector("input[type='text']");
|
34
|
+
if (schemeUrlElement) {
|
35
|
+
schemeUrlElement.value = null;
|
36
|
+
}
|
37
|
+
|
38
|
+
this.smCoverTarget.style.display = "none";
|
39
|
+
const smCoverElement = this.smCoverTarget.querySelector("input[type='file']");
|
40
|
+
if (smCoverElement) {
|
41
|
+
smCoverElement.value = null;
|
42
|
+
}
|
43
|
+
|
44
|
+
this.mdCoverTarget.style.display = "none";
|
45
|
+
const mdCoverElement = this.mdCoverTarget.querySelector("input[type='file']");
|
46
|
+
if (mdCoverElement) {
|
47
|
+
mdCoverElement.value = null;
|
48
|
+
}
|
49
|
+
|
50
|
+
this.lgCoverTarget.style.display = "none";
|
51
|
+
const lgCoverElement = this.lgCoverTarget.querySelector("input[type='file']");
|
52
|
+
if (lgCoverElement) {
|
53
|
+
lgCoverElement.value = null;
|
54
|
+
}
|
55
|
+
|
56
|
+
this.baseCoverTarget.style.display = "none";
|
57
|
+
const baseCoverElement = this.baseCoverTarget.querySelector("input[type='file']");
|
58
|
+
if (baseCoverElement) {
|
59
|
+
baseCoverElement.value = null;
|
60
|
+
}
|
61
|
+
|
62
|
+
this.splashCountdownTarget.style.display = "block";
|
63
|
+
this.splashUiTypeTarget.style.display = "block";
|
64
|
+
} else {
|
65
|
+
// 非dsp类广告,显示所有自定义元素
|
66
|
+
this.splashCountdownTarget.style.display = "block";
|
67
|
+
this.splashUiTypeTarget.style.display = "block";
|
68
|
+
this.smCoverTarget.style.display = "block";
|
69
|
+
this.mdCoverTarget.style.display = "block";
|
70
|
+
this.lgCoverTarget.style.display = "block";
|
71
|
+
this.baseCoverTarget.style.display = "block";
|
72
|
+
this.schemeUrlTarget.style.display = "block";
|
73
|
+
}
|
74
|
+
} else {
|
75
|
+
if (this.isDspAdValue) {
|
76
|
+
// 非开屏且为dsp类型广告,隐藏所有开屏参数
|
77
|
+
this.schemeUrlTarget.style.display = "none";
|
78
|
+
const schemeUrlElement = this.schemeUrlTarget.querySelector("input[type='text']");
|
79
|
+
if (schemeUrlElement) {
|
80
|
+
schemeUrlElement.value = null;
|
81
|
+
}
|
82
|
+
|
83
|
+
this.splashCountdownTarget.style.display = "none";
|
84
|
+
const splashCountdownElement = this.splashCountdownTarget.querySelector("input[type='number']");
|
85
|
+
if (splashCountdownElement) {
|
86
|
+
splashCountdownElement.value = 5;
|
87
|
+
}
|
88
|
+
|
89
|
+
this.splashUiTypeTarget.style.display = "none";
|
90
|
+
const splashUiTypeElement = this.splashUiTypeTarget.querySelector("select");
|
91
|
+
if (splashUiTypeElement) {
|
92
|
+
splashUiTypeElement.value = "half";
|
93
|
+
}
|
94
|
+
|
95
|
+
this.smCoverTarget.style.display = "none";
|
96
|
+
const smCoverElement = this.smCoverTarget.querySelector("input[type='file']");
|
97
|
+
if (smCoverElement) {
|
98
|
+
smCoverElement.value = null;
|
99
|
+
}
|
100
|
+
|
101
|
+
this.mdCoverTarget.style.display = "none";
|
102
|
+
const mdCoverElement = this.mdCoverTarget.querySelector("input[type='file']");
|
103
|
+
if (mdCoverElement) {
|
104
|
+
mdCoverElement.value = null;
|
105
|
+
}
|
106
|
+
|
107
|
+
this.lgCoverTarget.style.display = "none";
|
108
|
+
const lgCoverElement = this.lgCoverTarget.querySelector("input[type='file']");
|
109
|
+
if (lgCoverElement) {
|
110
|
+
lgCoverElement.value = null;
|
111
|
+
}
|
112
|
+
|
113
|
+
this.baseCoverTarget.style.display = "none";
|
114
|
+
const baseCoverElement = this.baseCoverTarget.querySelector("input[type='file']");
|
115
|
+
if (baseCoverElement) {
|
116
|
+
baseCoverElement.value = null;
|
117
|
+
}
|
118
|
+
} else {
|
119
|
+
// 非dsp、非开屏,保留schemeUrl、baseCover,其它类型都隐藏
|
120
|
+
this.schemeUrlTarget.style.display = "block";
|
121
|
+
this.baseCoverTarget.style.display = "block";
|
122
|
+
|
123
|
+
this.splashCountdownTarget.style.display = "none";
|
124
|
+
const splashCountdownElement = this.splashCountdownTarget.querySelector("input[type='number']");
|
125
|
+
if (splashCountdownElement) {
|
126
|
+
splashCountdownElement.value = 5;
|
127
|
+
}
|
128
|
+
|
129
|
+
this.splashUiTypeTarget.style.display = "none";
|
130
|
+
const splashUiTypeElement = this.splashUiTypeTarget.querySelector("select");
|
131
|
+
if (splashUiTypeElement) {
|
132
|
+
splashUiTypeElement.value = "half";
|
133
|
+
}
|
134
|
+
this.smCoverTarget.style.display = "none";
|
135
|
+
const smCoverElement = this.smCoverTarget.querySelector("input[type='file']");
|
136
|
+
if (smCoverElement) {
|
137
|
+
smCoverElement.value = null;
|
138
|
+
}
|
139
|
+
|
140
|
+
this.mdCoverTarget.style.display = "none";
|
141
|
+
const mdCoverElement = this.mdCoverTarget.querySelector("input[type='file']");
|
142
|
+
if (mdCoverElement) {
|
143
|
+
mdCoverElement.value = null;
|
144
|
+
}
|
145
|
+
this.lgCoverTarget.style.display = "none";
|
146
|
+
const lgCoverElement = this.lgCoverTarget.querySelector("input[type='file']");
|
147
|
+
if (lgCoverElement) {
|
148
|
+
lgCoverElement.value = null;
|
149
|
+
}
|
150
|
+
}
|
151
|
+
}
|
152
|
+
}
|
153
|
+
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import { destroy } from '@rails/request.js'
|
3
|
+
|
4
|
+
// Connects to data-controller="image-upload"
|
5
|
+
export default class extends Controller {
|
6
|
+
static targets = ["input", "preview", "removeButton"]
|
7
|
+
static values = {
|
8
|
+
url: {type: String, default: null},
|
9
|
+
}
|
10
|
+
|
11
|
+
connect() {
|
12
|
+
this.inputTarget.addEventListener("change", this.preview.bind(this))
|
13
|
+
this.removeButtonTarget.addEventListener("click", this.remove.bind(this))
|
14
|
+
}
|
15
|
+
|
16
|
+
preview() {
|
17
|
+
const file = this.inputTarget.files[0]
|
18
|
+
|
19
|
+
if (file) {
|
20
|
+
const reader = new FileReader()
|
21
|
+
reader.onload = (event) => {
|
22
|
+
this.previewTarget.src = event.target.result
|
23
|
+
this.removeButtonTarget.style.display = "block" // 显示移除按钮
|
24
|
+
}
|
25
|
+
reader.readAsDataURL(file)
|
26
|
+
} else {
|
27
|
+
this.previewTarget.src = ""
|
28
|
+
this.removeButtonTarget.style.display = "none" // 隐藏移除按钮
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
async remove() {
|
33
|
+
if (this.urlValue) {
|
34
|
+
const response = await destroy(this.urlValue)
|
35
|
+
if (response.ok) {
|
36
|
+
this.inputTarget.value = "" // 清空文件输入框的值
|
37
|
+
this.previewTarget.src = "" // 清空预览
|
38
|
+
this.removeButtonTarget.style.display = "none" // 隐藏移除按钮
|
39
|
+
}
|
40
|
+
} else {
|
41
|
+
this.inputTarget.value = "" // 清空文件输入框的值
|
42
|
+
this.previewTarget.src = "" // 清空预览
|
43
|
+
this.removeButtonTarget.style.display = "none" // 隐藏移除按钮
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
// This file is auto-generated by ./bin/rails stimulus:manifest:update
|
2
|
+
// Run that command whenever you add a new controller or create them with
|
3
|
+
// ./bin/rails generate stimulus controllerName
|
4
|
+
|
5
|
+
import { application } from "./application"
|
6
|
+
|
7
|
+
import HelloController from "./hello_controller"
|
8
|
+
application.register("hello", HelloController)
|
9
|
+
|
10
|
+
import ImageUploadController from "./image_upload_controller"
|
11
|
+
application.register("image-upload", ImageUploadController)
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module AdSpace
|
2
|
+
class Advertisement < ApplicationRecord
|
3
|
+
before_validation :generate_uuid, on: :create
|
4
|
+
|
5
|
+
enum platform: { ios: 0, android: 1 }, _suffix: true
|
6
|
+
enum ad_type: { dsp: 0, custom: 1 }, _suffix: true
|
7
|
+
enum splash_ui_type: { half: 0, full: 1 }, _suffix: true
|
8
|
+
enum visibility: { all: 0, regular_user: 2, premium_member: 1 }, _suffix: true
|
9
|
+
enum dsp_type: { gdt: 0, jrtt: 1, google: 2 }, _suffix: true
|
10
|
+
|
11
|
+
validates :name, presence: true, uniqueness: true
|
12
|
+
validates :uuid, presence: true, uniqueness: true
|
13
|
+
validates :ad_type, presence: true
|
14
|
+
|
15
|
+
validates :started_at, presence: true
|
16
|
+
validates :ended_at, presence: true
|
17
|
+
validate :valid_date_range
|
18
|
+
|
19
|
+
validates :platform, presence: true, inclusion: { in: platforms.keys }
|
20
|
+
validates :scheme_url, allow_blank: true, url_or_custom_scheme: true, if: -> { custom_ad_type? }
|
21
|
+
|
22
|
+
belongs_to :slot, class_name: "AdSpace::Slot", foreign_key: "slot_id"
|
23
|
+
delegate :splash?, to: :slot, prefix: false
|
24
|
+
|
25
|
+
has_one_attached :cover_sm, dependent: :destroy
|
26
|
+
has_one_attached :cover_base, dependent: :destroy
|
27
|
+
has_one_attached :cover_md, dependent: :destroy
|
28
|
+
has_one_attached :cover_lg, dependent: :destroy
|
29
|
+
|
30
|
+
scope :published, -> { where.not(published_at: nil) }
|
31
|
+
scope :unexpired, -> { where("started_at <= ?", Time.zone.now + 15.days).where("ended_at >= ?", Time.zone.now) }
|
32
|
+
|
33
|
+
def published?
|
34
|
+
published_at.present?
|
35
|
+
end
|
36
|
+
|
37
|
+
def archived?
|
38
|
+
archived_at.present?
|
39
|
+
end
|
40
|
+
|
41
|
+
def cover_url(size = nil)
|
42
|
+
if object.splash?
|
43
|
+
return cover_sm.attached? ? cover_sm.url : nil if size == 'little'
|
44
|
+
return cover_md.attached? ? cover_md.url : nil if size == 'middle'
|
45
|
+
return cover_lg.attached? ? cover_lg.url : nil if size == 'large'
|
46
|
+
else
|
47
|
+
cover_base.attached? ? cover_base.url : nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def valid_date_range
|
54
|
+
if started_at.present? && ended_at.present? && started_at > ended_at
|
55
|
+
errors.add(:ended_at, "must be greater than or equal to started_at")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def generate_uuid
|
60
|
+
if self.uuid.blank?
|
61
|
+
begin
|
62
|
+
self.uuid = SecureRandom.uuid
|
63
|
+
end while self.class.exists?(uuid: uuid)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module AdSpace
|
2
|
+
class Slot < ApplicationRecord
|
3
|
+
has_many :advertisements, dependent: :destroy
|
4
|
+
|
5
|
+
validates :name, presence: true, uniqueness: true
|
6
|
+
validates :uuid, presence: true, uniqueness: true
|
7
|
+
validates :remark, length: { maximum: 250 }
|
8
|
+
|
9
|
+
before_validation :generate_uuid, on: :create
|
10
|
+
|
11
|
+
has_one_attached :demo, dependent: :destroy
|
12
|
+
|
13
|
+
def splash?
|
14
|
+
is_splash
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def generate_uuid
|
20
|
+
if self.uuid.blank?
|
21
|
+
begin
|
22
|
+
self.uuid = SecureRandom.uuid
|
23
|
+
end while self.class.exists?(uuid: uuid)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|