ad_space 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|