diff --git a/internal/securitymanager/security_config.go b/internal/securitymanager/security_config.go index 15b15eaa..dca9f0bd 100644 --- a/internal/securitymanager/security_config.go +++ b/internal/securitymanager/security_config.go @@ -22,7 +22,10 @@ const ( var sharedSecurityConfig *SecurityConfig = nil type SecurityConfig struct { - Frame string `json:"frame"` + Frame string `json:"frame"` + AllowCountryIds []int64 `json:"allowCountryIds"` + AllowProvinceIds []int64 `json:"allowProvinceIds"` + AllowLocal bool `json:"allowLocal"` } func LoadSecurityConfig() (*SecurityConfig, error) { @@ -50,7 +53,7 @@ func UpdateSecurityConfig(securityConfig *SecurityConfig) error { if err != nil { return err } - _, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(1), &pb.UpdateSysSettingRequest{ + _, err = rpcClient.SysSettingRPC().UpdateSysSetting(rpcClient.Context(0), &pb.UpdateSysSettingRequest{ Code: SecuritySettingName, ValueJSON: valueJSON, }) @@ -69,7 +72,7 @@ func loadSecurityConfig() (*SecurityConfig, error) { if err != nil { return nil, err } - resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(1), &pb.ReadSysSettingRequest{ + resp, err := rpcClient.SysSettingRPC().ReadSysSetting(rpcClient.Context(0), &pb.ReadSysSettingRequest{ Code: SecuritySettingName, }) if err != nil { @@ -93,6 +96,7 @@ func loadSecurityConfig() (*SecurityConfig, error) { func defaultSecurityConfig() *SecurityConfig { return &SecurityConfig{ - Frame: FrameSameOrigin, + Frame: FrameSameOrigin, + AllowLocal: true, } } diff --git a/internal/tasks/task_sync_cluster.go b/internal/tasks/task_sync_cluster.go index 69fdb680..df4f22b3 100644 --- a/internal/tasks/task_sync_cluster.go +++ b/internal/tasks/task_sync_cluster.go @@ -41,7 +41,7 @@ func (this *SyncClusterTask) loop() error { if err != nil { return err } - ctx := rpcClient.Context(1) + ctx := rpcClient.Context(0) resp, err := rpcClient.NodeClusterRPC().FindAllChangedNodeClusters(ctx, &pb.FindAllChangedNodeClustersRequest{}) if err != nil { return err diff --git a/internal/utils/strings.go b/internal/utils/strings.go index d17693b6..1c83b53c 100644 --- a/internal/utils/strings.go +++ b/internal/utils/strings.go @@ -1,6 +1,9 @@ package utils -import "strings" +import ( + "github.com/iwind/TeaGo/types" + "strings" +) // format address func FormatAddress(addr string) string { @@ -13,3 +16,16 @@ func FormatAddress(addr string) string { addr = strings.TrimSpace(addr) return addr } + +// 分割数字 +func SplitNumbers(numbers string) (result []int64) { + if len(numbers) == 0 { + return + } + pieces := strings.Split(numbers, ",") + for _, piece := range pieces { + number := types.Int64(strings.TrimSpace(piece)) + result = append(result, number) + } + return +} diff --git a/internal/web/actions/default/settings/security/index.go b/internal/web/actions/default/settings/security/index.go index 99a26b7f..68f4d100 100644 --- a/internal/web/actions/default/settings/security/index.go +++ b/internal/web/actions/default/settings/security/index.go @@ -1,9 +1,12 @@ package security import ( + "encoding/json" "github.com/TeaOSLab/EdgeAdmin/internal/securitymanager" "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/iwind/TeaGo/actions" + "github.com/iwind/TeaGo/maps" ) type IndexAction struct { @@ -20,12 +23,52 @@ func (this *IndexAction) RunGet(params struct{}) { this.ErrorPage(err) return } + + // 国家和地区 + countryMaps := []maps.Map{} + for _, countryId := range config.AllowCountryIds { + countryResp, err := this.RPC().RegionCountryRPC().FindEnabledRegionCountry(this.AdminContext(), &pb.FindEnabledRegionCountryRequest{CountryId: countryId}) + if err != nil { + this.ErrorPage(err) + return + } + country := countryResp.Country + if country != nil { + countryMaps = append(countryMaps, maps.Map{ + "id": country.Id, + "name": country.Name, + }) + } + } + this.Data["countries"] = countryMaps + + // 省份 + provinceMaps := []maps.Map{} + for _, provinceId := range config.AllowProvinceIds { + provinceResp, err := this.RPC().RegionProvinceRPC().FindEnabledRegionProvince(this.AdminContext(), &pb.FindEnabledRegionProvinceRequest{ProvinceId: provinceId}) + if err != nil { + this.ErrorPage(err) + return + } + province := provinceResp.Province + if province != nil { + provinceMaps = append(provinceMaps, maps.Map{ + "id": province.Id, + "name": province.Name, + }) + } + } + this.Data["provinces"] = provinceMaps + this.Data["config"] = config this.Show() } func (this *IndexAction) RunPost(params struct { - Frame string + Frame string + CountryIdsJSON []byte + ProvinceIdsJSON []byte + AllowLocal bool Must *actions.Must CSRF *actionutils.CSRF @@ -38,7 +81,34 @@ func (this *IndexAction) RunPost(params struct { return } + // 框架 config.Frame = params.Frame + + // 国家和地区 + countryIds := []int64{} + if len(params.CountryIdsJSON) > 0 { + err = json.Unmarshal(params.CountryIdsJSON, &countryIds) + if err != nil { + this.ErrorPage(err) + return + } + } + config.AllowCountryIds = countryIds + + // 省份 + provinceIds := []int64{} + if len(params.ProvinceIdsJSON) > 0 { + err = json.Unmarshal(params.ProvinceIdsJSON, &provinceIds) + if err != nil { + this.ErrorPage(err) + return + } + } + config.AllowProvinceIds = provinceIds + + // 允许本地 + config.AllowLocal = params.AllowLocal + err = securitymanager.UpdateSecurityConfig(config) if err != nil { this.ErrorPage(err) diff --git a/internal/web/actions/default/ui/init.go b/internal/web/actions/default/ui/init.go index ea127400..2a519fad 100644 --- a/internal/web/actions/default/ui/init.go +++ b/internal/web/actions/default/ui/init.go @@ -11,6 +11,8 @@ func init() { server. Prefix("/ui"). Get("/download", new(DownloadAction)). + GetPost("/selectProvincesPopup", new(SelectProvincesPopupAction)). + GetPost("/selectCountriesPopup", new(SelectCountriesPopupAction)). // 以下的需要压缩 Helper(&actions.Gzip{Level: gzip.BestCompression}). diff --git a/internal/web/actions/default/ui/selectCountriesPopup.go b/internal/web/actions/default/ui/selectCountriesPopup.go new file mode 100644 index 00000000..23a833fb --- /dev/null +++ b/internal/web/actions/default/ui/selectCountriesPopup.go @@ -0,0 +1,70 @@ +package ui + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/utils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/actions" + "github.com/iwind/TeaGo/lists" + "github.com/iwind/TeaGo/maps" + "strings" +) + +type SelectCountriesPopupAction struct { + actionutils.ParentAction +} + +func (this *SelectCountriesPopupAction) Init() { + this.Nav("", "", "") +} + +func (this *SelectCountriesPopupAction) RunGet(params struct { + CountryIds string +}) { + selectedCountryIds := utils.SplitNumbers(params.CountryIds) + + countriesResp, err := this.RPC().RegionCountryRPC().FindAllEnabledRegionCountries(this.AdminContext(), &pb.FindAllEnabledRegionCountriesRequest{}) + if err != nil { + this.ErrorPage(err) + return + } + countryMaps := []maps.Map{} + for _, country := range countriesResp.Countries { + countryMaps = append(countryMaps, maps.Map{ + "id": country.Id, + "name": country.Name, + "letter": strings.ToUpper(string(country.Pinyin[0][0])), + "isChecked": lists.ContainsInt64(selectedCountryIds, country.Id), + }) + } + this.Data["countries"] = countryMaps + + this.Show() +} + +func (this *SelectCountriesPopupAction) RunPost(params struct { + CountryIds []int64 + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + countryMaps := []maps.Map{} + for _, countryId := range params.CountryIds { + countryResp, err := this.RPC().RegionCountryRPC().FindEnabledRegionCountry(this.AdminContext(), &pb.FindEnabledRegionCountryRequest{CountryId: countryId}) + if err != nil { + this.ErrorPage(err) + return + } + country := countryResp.Country + if country == nil { + continue + } + countryMaps = append(countryMaps, maps.Map{ + "id": country.Id, + "name": country.Name, + }) + } + this.Data["countries"] = countryMaps + + this.Success() +} diff --git a/internal/web/actions/default/ui/selectProvincesPopup.go b/internal/web/actions/default/ui/selectProvincesPopup.go new file mode 100644 index 00000000..e9448a90 --- /dev/null +++ b/internal/web/actions/default/ui/selectProvincesPopup.go @@ -0,0 +1,70 @@ +package ui + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/utils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/actions" + "github.com/iwind/TeaGo/lists" + "github.com/iwind/TeaGo/maps" +) + +const ChinaCountryId = 1 + +type SelectProvincesPopupAction struct { + actionutils.ParentAction +} + +func (this *SelectProvincesPopupAction) Init() { + this.Nav("", "", "") +} + +func (this *SelectProvincesPopupAction) RunGet(params struct { + ProvinceIds string +}) { + selectedProvinceIds := utils.SplitNumbers(params.ProvinceIds) + + provincesResp, err := this.RPC().RegionProvinceRPC().FindAllEnabledRegionProvincesWithCountryId(this.AdminContext(), &pb.FindAllEnabledRegionProvincesWithCountryIdRequest{CountryId: ChinaCountryId}) + if err != nil { + this.ErrorPage(err) + return + } + provinceMaps := []maps.Map{} + for _, province := range provincesResp.Provinces { + provinceMaps = append(provinceMaps, maps.Map{ + "id": province.Id, + "name": province.Name, + "isChecked": lists.ContainsInt64(selectedProvinceIds, province.Id), + }) + } + this.Data["provinces"] = provinceMaps + + this.Show() +} + +func (this *SelectProvincesPopupAction) RunPost(params struct { + ProvinceIds []int64 + + Must *actions.Must + CSRF *actionutils.CSRF +}) { + provinceMaps := []maps.Map{} + for _, provinceId := range params.ProvinceIds { + provinceResp, err := this.RPC().RegionProvinceRPC().FindEnabledRegionProvince(this.AdminContext(), &pb.FindEnabledRegionProvinceRequest{ProvinceId: provinceId}) + if err != nil { + this.ErrorPage(err) + return + } + province := provinceResp.Province + if province == nil { + continue + } + provinceMaps = append(provinceMaps, maps.Map{ + "id": province.Id, + "name": province.Name, + }) + } + this.Data["provinces"] = provinceMaps + + this.Success() +} diff --git a/internal/web/helpers/user_must_auth.go b/internal/web/helpers/user_must_auth.go index bdea340a..05c6f7f9 100644 --- a/internal/web/helpers/user_must_auth.go +++ b/internal/web/helpers/user_must_auth.go @@ -35,6 +35,12 @@ func (this *UserMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam } action.AddHeader("Content-Security-Policy", "default-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'") + // 检查IP + if !checkIP(securityConfig, action.RequestRemoteIP()) { + action.ResponseWriter.WriteHeader(http.StatusForbidden) + return false + } + // 检查系统是否已经配置过 if !setup.IsConfigured() { action.RedirectURL("/setup") @@ -82,8 +88,48 @@ func (this *UserMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam return action.Data["teaTitle"].(string) }) - // 初始化变量 - modules := []maps.Map{ + action.Data["teaTitle"] = teaconst.ProductNameZH + action.Data["teaName"] = teaconst.ProductNameZH + + resp, err := rpc.AdminRPC().FindAdminFullname(rpc.Context(0), &pb.FindAdminFullnameRequest{AdminId: int64(this.AdminId)}) + if err != nil { + utils.PrintError(err) + action.Data["teaUsername"] = "" + } else { + action.Data["teaUsername"] = resp.Fullname + } + + action.Data["teaUserAvatar"] = "" + + action.Data["teaMenu"] = "" + action.Data["teaModules"] = this.modules() + action.Data["teaSubMenus"] = []map[string]interface{}{} + action.Data["teaTabbar"] = []map[string]interface{}{} + action.Data["teaVersion"] = teaconst.Version + action.Data["teaIsSuper"] = false + action.Data["teaDemoEnabled"] = teaconst.IsDemo + if !action.Data.Has("teaSubMenu") { + action.Data["teaSubMenu"] = "" + } + + // 菜单 + action.Data["firstMenuItem"] = "" + + // 未读消息数 + action.Data["teaBadge"] = 0 + + // 调用Init + initMethod := reflect.ValueOf(actionPtr).MethodByName("Init") + if initMethod.IsValid() { + initMethod.Call([]reflect.Value{}) + } + + return true +} + +// 菜单配置 +func (this *UserMustAuth) modules() []maps.Map { + return []maps.Map{ { "code": "servers", "name": "网站服务", @@ -136,46 +182,9 @@ func (this *UserMustAuth) BeforeAction(actionPtr actions.ActionWrapper, paramNam "icon": "history", }, } - - action.Data["teaTitle"] = teaconst.ProductNameZH - action.Data["teaName"] = teaconst.ProductNameZH - - resp, err := rpc.AdminRPC().FindAdminFullname(rpc.Context(0), &pb.FindAdminFullnameRequest{AdminId: int64(this.AdminId)}) - if err != nil { - utils.PrintError(err) - action.Data["teaUsername"] = "" - } else { - action.Data["teaUsername"] = resp.Fullname - } - - action.Data["teaUserAvatar"] = "" - - action.Data["teaMenu"] = "" - action.Data["teaModules"] = modules - action.Data["teaSubMenus"] = []map[string]interface{}{} - action.Data["teaTabbar"] = []map[string]interface{}{} - action.Data["teaVersion"] = teaconst.Version - action.Data["teaIsSuper"] = false - action.Data["teaDemoEnabled"] = teaconst.IsDemo - if !action.Data.Has("teaSubMenu") { - action.Data["teaSubMenu"] = "" - } - - // 菜单 - action.Data["firstMenuItem"] = "" - - // 未读消息数 - action.Data["teaBadge"] = 0 - - // 调用Init - initMethod := reflect.ValueOf(actionPtr).MethodByName("Init") - if initMethod.IsValid() { - initMethod.Call([]reflect.Value{}) - } - - return true } +// 跳转到登录页 func (this *UserMustAuth) login(action *actions.ActionObject) { action.RedirectURL("/") } diff --git a/internal/web/helpers/user_should_auth.go b/internal/web/helpers/user_should_auth.go index 54bc9f0a..f78eab13 100644 --- a/internal/web/helpers/user_should_auth.go +++ b/internal/web/helpers/user_should_auth.go @@ -24,6 +24,12 @@ func (this *UserShouldAuth) BeforeAction(actionPtr actions.ActionWrapper, paramN } action.AddHeader("Content-Security-Policy", "default-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'") + // 检查IP + if !checkIP(securityConfig, action.RequestRemoteIP()) { + action.ResponseWriter.WriteHeader(http.StatusForbidden) + return false + } + return true } diff --git a/internal/web/helpers/utils.go b/internal/web/helpers/utils.go new file mode 100644 index 00000000..6eb10c37 --- /dev/null +++ b/internal/web/helpers/utils.go @@ -0,0 +1,60 @@ +package helpers + +import ( + nodes "github.com/TeaOSLab/EdgeAdmin/internal/rpc" + "github.com/TeaOSLab/EdgeAdmin/internal/securitymanager" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/lists" + "github.com/iwind/TeaGo/logs" + "net" +) + +// 检查用户IP +func checkIP(config *securitymanager.SecurityConfig, ipAddr string) bool { + if config == nil { + return true + } + + // 本地IP + ip := net.ParseIP(ipAddr).To4() + if ip == nil { + logs.Println("[USER_MUST_AUTH]invalid client address: " + ipAddr) + return false + } + if config.AllowLocal && isLocalIP(ip) { + return true + } + + // 检查位置 + if len(config.AllowCountryIds) > 0 || len(config.AllowProvinceIds) > 0 { + rpc, err := nodes.SharedRPC() + if err != nil { + logs.Println("[USER_MUST_AUTH][ERROR]" + err.Error()) + return false + } + resp, err := rpc.IPLibraryRPC().LookupIPRegion(rpc.Context(0), &pb.LookupIPRegionRequest{Ip: ipAddr}) + if err != nil { + logs.Println("[USER_MUST_AUTH][ERROR]" + err.Error()) + return false + } + if resp.Region == nil { + return true + } + if len(config.AllowCountryIds) > 0 && !lists.ContainsInt64(config.AllowCountryIds, resp.Region.CountryId) { + return false + } + if len(config.AllowProvinceIds) > 0 && !lists.ContainsInt64(config.AllowProvinceIds, resp.Region.ProvinceId) { + return false + } + } + + return true +} + +// 判断是否为本地IP +func isLocalIP(ip net.IP) bool { + return ip[0] == 127 || + ip[0] == 10 || + (ip[0] == 172 && ip[1]&0xf0 == 16) || + (ip[0] == 192 && ip[1] == 168) +} diff --git a/web/public/js/components/common/countries-selector.js b/web/public/js/components/common/countries-selector.js new file mode 100644 index 00000000..cf423b06 --- /dev/null +++ b/web/public/js/components/common/countries-selector.js @@ -0,0 +1,51 @@ +Vue.component("countries-selector", { + props: ["v-countries"], + data: function () { + let countries = this.vCountries + if (countries == null) { + countries = [] + } + let countryIds = countries.$map(function (k, v) { + return v.id + }) + return { + countries: countries, + countryIds: countryIds + } + }, + methods: { + add: function () { + let countryStringIds = this.countryIds.map(function (v) { + return v.toString() + }) + let that = this + teaweb.popup("/ui/selectCountriesPopup?countryIds=" + countryStringIds.join(","), { + width: "48em", + height: "23em", + callback: function (resp) { + that.countries = resp.data.countries + that.change() + } + }) + }, + remove: function (index) { + this.countries.$remove(index) + this.change() + }, + change: function () { + this.countryIds = this.countries.$map(function (k, v) { + return v.id + }) + } + }, + template: `
+ +
+
{{country.name}}
+
+
+
+ +
+
` +}) \ No newline at end of file diff --git a/web/public/js/components/common/provinces-selector.js b/web/public/js/components/common/provinces-selector.js new file mode 100644 index 00000000..e2bb458a --- /dev/null +++ b/web/public/js/components/common/provinces-selector.js @@ -0,0 +1,51 @@ +Vue.component("provinces-selector", { + props: ["v-provinces"], + data: function () { + let provinces = this.vProvinces + if (provinces == null) { + provinces = [] + } + let provinceIds = provinces.$map(function (k, v) { + return v.id + }) + return { + provinces: provinces, + provinceIds: provinceIds + } + }, + methods: { + add: function () { + let provinceStringIds = this.provinceIds.map(function (v) { + return v.toString() + }) + let that = this + teaweb.popup("/ui/selectProvincesPopup?provinceIds=" + provinceStringIds.join(","), { + width: "48em", + height: "23em", + callback: function (resp) { + that.provinces = resp.data.provinces + that.change() + } + }) + }, + remove: function (index) { + this.provinces.$remove(index) + this.change() + }, + change: function () { + this.provinceIds = this.provinces.$map(function (k, v) { + return v.id + }) + } + }, + template: `
+ +
+
{{province.name}}
+
+
+
+ +
+
` +}) \ No newline at end of file diff --git a/web/views/@default/@layout.html b/web/views/@default/@layout.html index 0d0089d0..073bf342 100644 --- a/web/views/@default/@layout.html +++ b/web/views/@default/@layout.html @@ -12,7 +12,7 @@ {$TEA.VUE} {$echo "header"} - + diff --git a/web/views/@default/@layout_popup.html b/web/views/@default/@layout_popup.html index 3e78d08c..ccfdc186 100644 --- a/web/views/@default/@layout_popup.html +++ b/web/views/@default/@layout_popup.html @@ -12,7 +12,7 @@ {$echo "header"} - + diff --git a/web/views/@default/settings/security/index.html b/web/views/@default/settings/security/index.html index c2081fcf..1faac34b 100644 --- a/web/views/@default/settings/security/index.html +++ b/web/views/@default/settings/security/index.html @@ -15,6 +15,27 @@

当前服务被别的网页框架嵌套的条件限制。

+ + 允许访问的国家和地区 + + +

设置后,只有这些国家和地区才能访问管理界面,如果不设置表示没有限制。

+ + + + 允许访问的省份(中国) + + +

设置后,只有这些省份才能访问管理界面,如果不设置表示没有限制。

+ + + + 允许局域网访问 + + +

选中表示允许在本机和局域网访问。

+ + \ No newline at end of file diff --git a/web/views/@default/ui/selectCountriesPopup.css b/web/views/@default/ui/selectCountriesPopup.css new file mode 100644 index 00000000..901b146c --- /dev/null +++ b/web/views/@default/ui/selectCountriesPopup.css @@ -0,0 +1,16 @@ +.region-letter-group .item { + padding-left: 1em !important; + padding-right: 1em !important; +} +.country-group { + padding-bottom: 1em; +} +.country-group .country-list .item { + float: left; + width: 12em; + margin-bottom: 0.5em; +} +.country-group .country-list .item .checkbox label { + font-size: 12px !important; +} +/*# sourceMappingURL=selectCountriesPopup.css.map */ \ No newline at end of file diff --git a/web/views/@default/ui/selectCountriesPopup.css.map b/web/views/@default/ui/selectCountriesPopup.css.map new file mode 100644 index 00000000..eccb7adf --- /dev/null +++ b/web/views/@default/ui/selectCountriesPopup.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["selectCountriesPopup.less"],"names":[],"mappings":"AAAA,oBACC;EACC,4BAAA;EACA,6BAAA;;AAIF;EAaC,mBAAA;;AAbD,cACC,cACC;EACC,WAAA;EACA,WAAA;EACA,oBAAA;;AALH,cACC,cACC,MAKC,UAAU;EACT,0BAAA","file":"selectCountriesPopup.css"} \ No newline at end of file diff --git a/web/views/@default/ui/selectCountriesPopup.html b/web/views/@default/ui/selectCountriesPopup.html new file mode 100644 index 00000000..d55834c7 --- /dev/null +++ b/web/views/@default/ui/selectCountriesPopup.html @@ -0,0 +1,27 @@ +{$layout "layout_popup"} + +

选择国家和地区

+
+ + + + + +
+ +
+
+

{{letter}}

+
+
+ {{country.name}} +
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/web/views/@default/ui/selectCountriesPopup.js b/web/views/@default/ui/selectCountriesPopup.js new file mode 100644 index 00000000..e9512bad --- /dev/null +++ b/web/views/@default/ui/selectCountriesPopup.js @@ -0,0 +1,21 @@ +Tea.context(function () { + this.letterGroups = [ + "ABC", "DEF", "GHI", "JKL", "MNO", "PQR", "STU", "VWX", "YZ" + ]; + this.selectedGroup = "ABC" + this.letterCountries = {} + let that = this + this.countries.forEach(function (country) { + if (typeof (that.letterCountries[country.letter]) == "undefined") { + that.letterCountries[country.letter] = [] + } + that.letterCountries[country.letter].push(country) + }) + this.isCheckingAll = false + + this.selectGroup = function (group) { + this.selectedGroup = group + } + + this.success = NotifyPopup +}) \ No newline at end of file diff --git a/web/views/@default/ui/selectCountriesPopup.less b/web/views/@default/ui/selectCountriesPopup.less new file mode 100644 index 00000000..f6774ab9 --- /dev/null +++ b/web/views/@default/ui/selectCountriesPopup.less @@ -0,0 +1,22 @@ +.region-letter-group { + .item { + padding-left: 1em !important; + padding-right: 1em !important; + } +} + +.country-group { + .country-list { + .item { + float: left; + width: 12em; + margin-bottom: 0.5em; + + .checkbox label { + font-size: 12px !important; + } + } + } + + padding-bottom: 1em; +} \ No newline at end of file diff --git a/web/views/@default/ui/selectProvincesPopup.css b/web/views/@default/ui/selectProvincesPopup.css new file mode 100644 index 00000000..e9e190b0 --- /dev/null +++ b/web/views/@default/ui/selectProvincesPopup.css @@ -0,0 +1,9 @@ +.province-list .item { + float: left; + width: 12em; + margin-bottom: 0.5em; +} +.province-list .item .checkbox label { + font-size: 12px !important; +} +/*# sourceMappingURL=selectProvincesPopup.css.map */ \ No newline at end of file diff --git a/web/views/@default/ui/selectProvincesPopup.css.map b/web/views/@default/ui/selectProvincesPopup.css.map new file mode 100644 index 00000000..afb18f41 --- /dev/null +++ b/web/views/@default/ui/selectProvincesPopup.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["selectProvincesPopup.less"],"names":[],"mappings":"AAAA,cACC;EACC,WAAA;EACA,WAAA;EACA,oBAAA;;AAJF,cACC,MAKC,UAAU;EACT,0BAAA","file":"selectProvincesPopup.css"} \ No newline at end of file diff --git a/web/views/@default/ui/selectProvincesPopup.html b/web/views/@default/ui/selectProvincesPopup.html new file mode 100644 index 00000000..757beb23 --- /dev/null +++ b/web/views/@default/ui/selectProvincesPopup.html @@ -0,0 +1,19 @@ +{$layout "layout_popup"} + +

选择省份

+ +
+ + + + + +
+
+
+ {{province.name}} +
+
+
+ +
\ No newline at end of file diff --git a/web/views/@default/ui/selectProvincesPopup.js b/web/views/@default/ui/selectProvincesPopup.js new file mode 100644 index 00000000..c8fe9515 --- /dev/null +++ b/web/views/@default/ui/selectProvincesPopup.js @@ -0,0 +1,3 @@ +Tea.context(function () { + this.success = NotifyPopup +}) \ No newline at end of file diff --git a/web/views/@default/ui/selectProvincesPopup.less b/web/views/@default/ui/selectProvincesPopup.less new file mode 100644 index 00000000..8285ca83 --- /dev/null +++ b/web/views/@default/ui/selectProvincesPopup.less @@ -0,0 +1,11 @@ +.province-list { + .item { + float: left; + width: 12em; + margin-bottom: 0.5em; + + .checkbox label { + font-size: 12px !important; + } + } +} \ No newline at end of file