diff --git a/internal/configloaders/admin_module.go b/internal/configloaders/admin_module.go
index 0d14aba5..ab215075 100644
--- a/internal/configloaders/admin_module.go
+++ b/internal/configloaders/admin_module.go
@@ -19,6 +19,7 @@ const (
AdminModuleCodeAdmin AdminModuleCode = "admin" // 系统用户
AdminModuleCodeUser AdminModuleCode = "user" // 平台用户
AdminModuleCodeFinance AdminModuleCode = "finance" // 财务
+ AdminModuleCodePlan AdminModuleCode = "plan" // 套餐
AdminModuleCodeLog AdminModuleCode = "log" // 日志
AdminModuleCodeSetting AdminModuleCode = "setting" // 设置
AdminModuleCodeCommon AdminModuleCode = "common" // 只要登录就可以访问的模块
@@ -203,6 +204,11 @@ func AllModuleMaps() []maps.Map {
"code": AdminModuleCodeFinance,
"url": "/finance",
},
+ {
+ "name": "套餐管理",
+ "code": AdminModuleCodePlan,
+ "url": "/plans",
+ },
{
"name": "日志审计",
"code": AdminModuleCodeLog,
diff --git a/internal/rpc/rpc_client.go b/internal/rpc/rpc_client.go
index 4ed6e9e5..5e29ff0e 100644
--- a/internal/rpc/rpc_client.go
+++ b/internal/rpc/rpc_client.go
@@ -464,6 +464,14 @@ func (this *RPCClient) ServerStatBoardChartRPC() pb.ServerStatBoardChartServiceC
return pb.NewServerStatBoardChartServiceClient(this.pickConn())
}
+func (this *RPCClient) PlanRPC() pb.PlanServiceClient {
+ return pb.NewPlanServiceClient(this.pickConn())
+}
+
+func (this *RPCClient) UserPlanRPC() pb.UserPlanServiceClient {
+ return pb.NewUserPlanServiceClient(this.pickConn())
+}
+
// Context 构造Admin上下文
func (this *RPCClient) Context(adminId int64) context.Context {
ctx := context.Background()
diff --git a/internal/web/actions/actionutils/parent_action.go b/internal/web/actions/actionutils/parent_action.go
index 3ef8fed4..871e45b1 100644
--- a/internal/web/actions/actionutils/parent_action.go
+++ b/internal/web/actions/actionutils/parent_action.go
@@ -19,6 +19,8 @@ type ParentAction struct {
actions.ActionObject
rpcClient *rpc.RPCClient
+
+ ctx context.Context
}
// Parent 可以调用自身的一个简便方法
@@ -117,6 +119,10 @@ func (this *ParentAction) RPC() *rpc.RPCClient {
// AdminContext 获取Context
func (this *ParentAction) AdminContext() context.Context {
+ if this.ctx != nil {
+ return this.ctx
+ }
+
if this.rpcClient == nil {
rpcClient, err := rpc.SharedRPC()
if err != nil {
@@ -125,7 +131,8 @@ func (this *ParentAction) AdminContext() context.Context {
}
this.rpcClient = rpcClient
}
- return this.rpcClient.Context(this.AdminId())
+ this.ctx = this.rpcClient.Context(this.AdminId())
+ return this.ctx
}
// ViewData 视图里可以使用的数据
diff --git a/internal/web/helpers/user_must_auth.go b/internal/web/helpers/user_must_auth.go
index aedb7673..50c8d919 100644
--- a/internal/web/helpers/user_must_auth.go
+++ b/internal/web/helpers/user_must_auth.go
@@ -374,6 +374,21 @@ func (this *userMustAuth) modules(actionPtr actions.ActionWrapper, adminId int64
"icon": "yen sign",
"isOn": teaconst.IsPlus,
},
+ /**{
+ "code": "plans",
+ "module": configloaders.AdminModuleCodePlan,
+ "name": "套餐管理",
+ "icon": "puzzle piece",
+ "isOn": teaconst.IsPlus,
+ "subItems": []maps.Map{
+ {
+ "name": "已购套餐",
+ "url": "/plans/userPlans",
+ "code": "userPlans",
+ "isOn": teaconst.IsPlus,
+ },
+ },
+ },**/
{
"code": "admins",
"module": configloaders.AdminModuleCodeAdmin,
diff --git a/web/public/js/components/common/datepicker.js b/web/public/js/components/common/datepicker.js
new file mode 100644
index 00000000..5a6b22a8
--- /dev/null
+++ b/web/public/js/components/common/datepicker.js
@@ -0,0 +1,34 @@
+Vue.component("datepicker", {
+ props: ["v-name", "v-value", "v-bottom-left"],
+ mounted: function () {
+ let that = this
+ teaweb.datepicker(this.$refs.dayInput, function (v) {
+ that.day = v
+ that.change()
+ }, !!this.vBottomLeft)
+ },
+ data: function () {
+ let name = this.vName
+ if (name == null) {
+ name = "day"
+ }
+
+ let day = this.vValue
+ if (day == null) {
+ day = ""
+ }
+
+ return {
+ name: name,
+ day: day
+ }
+ },
+ methods: {
+ change: function () {
+ this.$emit("change", this.day)
+ }
+ },
+ template: `
+
+
`
+})
\ No newline at end of file
diff --git a/web/public/js/components/plans/plan-price-config-box.js b/web/public/js/components/plans/plan-price-config-box.js
new file mode 100644
index 00000000..b84f15e6
--- /dev/null
+++ b/web/public/js/components/plans/plan-price-config-box.js
@@ -0,0 +1,166 @@
+// 套餐价格配置
+Vue.component("plan-price-config-box", {
+ props: ["v-price-type", "v-monthly-price", "v-seasonally-price", "v-yearly-price", "v-bandwidth-price"],
+ data: function () {
+ let priceType = this.vPriceType
+ if (priceType == null) {
+ priceType = "period"
+ }
+
+ let monthlyPriceNumber = 0
+ let monthlyPrice = this.vMonthlyPrice
+ if (monthlyPrice == null || monthlyPrice <= 0) {
+ monthlyPrice = ""
+ } else {
+ monthlyPrice = monthlyPrice.toString()
+ monthlyPriceNumber = parseFloat(monthlyPrice)
+ if (isNaN(monthlyPriceNumber)) {
+ monthlyPriceNumber = 0
+ }
+ }
+
+ let seasonallyPriceNumber = 0
+ let seasonallyPrice = this.vSeasonallyPrice
+ if (seasonallyPrice == null || seasonallyPrice <= 0) {
+ seasonallyPrice = ""
+ } else {
+ seasonallyPrice = seasonallyPrice.toString()
+ seasonallyPriceNumber = parseFloat(seasonallyPrice)
+ if (isNaN(seasonallyPriceNumber)) {
+ seasonallyPriceNumber = 0
+ }
+ }
+
+ let yearlyPriceNumber = 0
+ let yearlyPrice = this.vYearlyPrice
+ if (yearlyPrice == null || yearlyPrice <= 0) {
+ yearlyPrice = ""
+ } else {
+ yearlyPrice = yearlyPrice.toString()
+ yearlyPriceNumber = parseFloat(yearlyPrice)
+ if (isNaN(yearlyPriceNumber)) {
+ yearlyPriceNumber = 0
+ }
+ }
+
+ let bandwidthPrice = this.vBandwidthPrice
+ let bandwidthPriceBaseNumber = 0
+ if (bandwidthPrice != null) {
+ bandwidthPriceBaseNumber = bandwidthPrice.base
+ } else {
+ bandwidthPrice = {
+ base: 0
+ }
+ }
+ let bandwidthPriceBase = ""
+ if (bandwidthPriceBaseNumber > 0) {
+ bandwidthPriceBase = bandwidthPriceBaseNumber.toString()
+ }
+
+ return {
+ priceType: priceType,
+ monthlyPrice: monthlyPrice,
+ seasonallyPrice: seasonallyPrice,
+ yearlyPrice: yearlyPrice,
+
+ monthlyPriceNumber: monthlyPriceNumber,
+ seasonallyPriceNumber: seasonallyPriceNumber,
+ yearlyPriceNumber: yearlyPriceNumber,
+
+ bandwidthPriceBase: bandwidthPriceBase,
+ bandwidthPrice: bandwidthPrice
+ }
+ },
+ watch: {
+ monthlyPrice: function (v) {
+ let price = parseFloat(v)
+ if (isNaN(price)) {
+ price = 0
+ }
+ this.monthlyPriceNumber = price
+ },
+ seasonallyPrice: function (v) {
+ let price = parseFloat(v)
+ if (isNaN(price)) {
+ price = 0
+ }
+ this.seasonallyPriceNumber = price
+ },
+ yearlyPrice: function (v) {
+ let price = parseFloat(v)
+ if (isNaN(price)) {
+ price = 0
+ }
+ this.yearlyPriceNumber = price
+ },
+ bandwidthPriceBase: function (v) {
+ let price = parseFloat(v)
+ if (isNaN(price)) {
+ price = 0
+ }
+ this.bandwidthPrice.base = price
+ }
+ },
+ template: `
+
+
+
+
+
+
+
+ 按时间周期
+ 按带宽用量
+
+
+
+
+
+
+
+
`
+})
\ No newline at end of file
diff --git a/web/public/js/components/plans/plan-price-view.js b/web/public/js/components/plans/plan-price-view.js
new file mode 100644
index 00000000..43174314
--- /dev/null
+++ b/web/public/js/components/plans/plan-price-view.js
@@ -0,0 +1,18 @@
+Vue.component("plan-price-view", {
+ props: ["v-plan"],
+ data: function () {
+ return {
+ plan: this.vPlan
+ }
+ },
+ template: `
+
+ 月度:¥{{plan.monthlyPrice}}元
+ 季度:¥{{plan.seasonallyPrice}}元
+ 年度:¥{{plan.yearlyPrice}}元
+
+
+ 基础价格:¥{{plan.bandwidthPrice.base}}元/GB
+
+
`
+})
\ No newline at end of file
diff --git a/web/public/js/components/plans/plan-user-selector.js b/web/public/js/components/plans/plan-user-selector.js
new file mode 100644
index 00000000..836a13eb
--- /dev/null
+++ b/web/public/js/components/plans/plan-user-selector.js
@@ -0,0 +1,28 @@
+Vue.component("plan-user-selector", {
+ mounted: function () {
+ let that = this
+
+ Tea.action("/plans/users/options")
+ .post()
+ .success(function (resp) {
+ that.users = resp.data.users
+ })
+ },
+ props: ["v-user-id"],
+ data: function () {
+ let userId = this.vUserId
+ if (userId == null) {
+ userId = 0
+ }
+ return {
+ users: [],
+ userId: userId
+ }
+ },
+ template: `
+
+
`
+})
\ No newline at end of file
diff --git a/web/public/js/components/server/bandwidth-limit-view.js b/web/public/js/components/server/bandwidth-limit-view.js
new file mode 100644
index 00000000..8ebd7f0d
--- /dev/null
+++ b/web/public/js/components/server/bandwidth-limit-view.js
@@ -0,0 +1,16 @@
+// 显示带宽限制说明
+Vue.component("bandwidth-limit-view", {
+ props: ["v-bandwidth-limit"],
+ data: function () {
+ return {
+ config: this.vBandwidthLimit
+ }
+ },
+ template: `
+
+ 日带宽限制:{{config.dailySize.count}}{{config.dailySize.unit.toUpperCase()}}
+ 月带宽限制:{{config.monthlySize.count}}{{config.monthlySize.unit.toUpperCase()}}
+
+
没有限制。
+
`
+})
\ No newline at end of file
diff --git a/web/public/js/utils.js b/web/public/js/utils.js
index 7350590a..8b563458 100644
--- a/web/public/js/utils.js
+++ b/web/public/js/utils.js
@@ -69,13 +69,13 @@ window.teaweb = {
}
document.head.append(element)
},
- datepicker: function (element, callback) {
+ datepicker: function (element, callback, bottomLeft) {
// 加载
if (typeof Pikaday == "undefined") {
let that = this
this.loadJS("/js/moment.min.js", function () {
that.loadJS("/js/pikaday.js", function () {
- that.datepicker(element, callback)
+ that.datepicker(element, callback, bottomLeft)
})
})
this.loadCSS("/js/pikaday.css")
@@ -89,8 +89,8 @@ window.teaweb = {
if (typeof (element) == "string") {
element = document.getElementById(element);
}
- var year = new Date().getFullYear();
- var picker = new Pikaday({
+ let year = new Date().getFullYear();
+ let picker = new Pikaday({
field: element,
firstDay: 1,
minDate: new Date(year - 1, 0, 1),
@@ -109,8 +109,9 @@ window.teaweb = {
if (typeof (callback) == "function") {
callback.call(Tea.Vue, picker.toString());
}
- }
- });
+ },
+ reposition: !bottomLeft
+ })
},
formatBytes: function (bytes) {
@@ -256,6 +257,21 @@ window.teaweb = {
}
});
},
+ popupSuccess: function (url, width, height) {
+ let options = {}
+ if (width != null) {
+ options["width"] = width
+ }
+ if (height != null) {
+ options["height"] = height
+ }
+ options["callback"] = function () {
+ teaweb.success("保存成功", function () {
+ teaweb.reload()
+ })
+ }
+ this.popup(url, options)
+ },
popupFinish: function () {
if (window.POPUP_CALLBACK != null) {
window.POPUP_CALLBACK.apply(window, arguments);
diff --git a/web/views/@default/servers/certs/acme/index.html b/web/views/@default/servers/certs/acme/index.html
index d5f6d0ab..bea95516 100644
--- a/web/views/@default/servers/certs/acme/index.html
+++ b/web/views/@default/servers/certs/acme/index.html
@@ -9,7 +9,7 @@
有效证书({{countAvailable}})
过期证书({{countExpired}})
7天内过期({{count7Days}})
- 30天过期({{count30Days}})
+ 30天内过期({{count30Days}})