mirror of
https://github.com/TeaOSLab/EdgeAdmin.git
synced 2025-11-02 20:00:26 +08:00
增加套餐相关代码
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 视图里可以使用的数据
|
||||
|
||||
@@ -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,
|
||||
|
||||
34
web/public/js/components/common/datepicker.js
Normal file
34
web/public/js/components/common/datepicker.js
Normal file
@@ -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: `<div style="display: inline-block">
|
||||
<input type="text" :name="name" v-model="day" placeholder="YYYY-MM-DD" style="width:8.6em" maxlength="10" @input="change" ref="dayInput" autocomplete="off"/>
|
||||
</div>`
|
||||
})
|
||||
166
web/public/js/components/plans/plan-price-config-box.js
Normal file
166
web/public/js/components/plans/plan-price-config-box.js
Normal file
@@ -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: `<div>
|
||||
<input type="hidden" name="priceType" :value="priceType"/>
|
||||
<input type="hidden" name="monthlyPrice" :value="monthlyPriceNumber"/>
|
||||
<input type="hidden" name="seasonallyPrice" :value="seasonallyPriceNumber"/>
|
||||
<input type="hidden" name="yearlyPrice" :value="yearlyPriceNumber"/>
|
||||
<input type="hidden" name="bandwidthPriceJSON" :value="JSON.stringify(bandwidthPrice)"/>
|
||||
|
||||
<div>
|
||||
<radio :v-value="'period'" :value="priceType" v-model="priceType"> 按时间周期</radio>
|
||||
<radio :v-value="'bandwidth'" :value="priceType" v-model="priceType"> 按带宽用量</radio>
|
||||
</div>
|
||||
|
||||
<!-- 按时间周期 -->
|
||||
<div v-show="priceType == 'period'">
|
||||
<div class="ui divider"></div>
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td class="title">月度价格</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" style="width: 7em" maxlength="10" v-model="monthlyPrice"/>
|
||||
<span class="ui label">元</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">季度价格</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" style="width: 7em" maxlength="10" v-model="seasonallyPrice"/>
|
||||
<span class="ui label">元</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">年度价格</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" style="width: 7em" maxlength="10" v-model="yearlyPrice"/>
|
||||
<span class="ui label">元</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 按带宽 -->
|
||||
<div v-show="priceType =='bandwidth'">
|
||||
<div class="ui divider"></div>
|
||||
<table class="ui table">
|
||||
<tr>
|
||||
<td class="title">基础带宽费用</td>
|
||||
<td>
|
||||
<div class="ui input right labeled">
|
||||
<input type="text" v-model="bandwidthPriceBase" maxlength="10" style="width: 7em"/>
|
||||
<span class="ui label">元/GB</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>`
|
||||
})
|
||||
18
web/public/js/components/plans/plan-price-view.js
Normal file
18
web/public/js/components/plans/plan-price-view.js
Normal file
@@ -0,0 +1,18 @@
|
||||
Vue.component("plan-price-view", {
|
||||
props: ["v-plan"],
|
||||
data: function () {
|
||||
return {
|
||||
plan: this.vPlan
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<span v-if="plan.priceType == 'period'">
|
||||
<span v-if="plan.monthlyPrice > 0">月度:¥{{plan.monthlyPrice}}元<br/></span>
|
||||
<span v-if="plan.seasonallyPrice > 0">季度:¥{{plan.seasonallyPrice}}元<br/></span>
|
||||
<span v-if="plan.yearlyPrice > 0">年度:¥{{plan.yearlyPrice}}元</span>
|
||||
</span>
|
||||
<span v-if="plan.priceType == 'bandwidth'">
|
||||
基础价格:¥{{plan.bandwidthPrice.base}}元/GB
|
||||
</span>
|
||||
</div>`
|
||||
})
|
||||
28
web/public/js/components/plans/plan-user-selector.js
Normal file
28
web/public/js/components/plans/plan-user-selector.js
Normal file
@@ -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: `<div>
|
||||
<select class="ui dropdown auto-width" name="userId" v-model="userId">
|
||||
<option value="0">[选择用户]</option>
|
||||
<option v-for="user in users" :value="user.id">{{user.fullname}} ({{user.username}})</option>
|
||||
</select>
|
||||
</div>`
|
||||
})
|
||||
16
web/public/js/components/server/bandwidth-limit-view.js
Normal file
16
web/public/js/components/server/bandwidth-limit-view.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// 显示带宽限制说明
|
||||
Vue.component("bandwidth-limit-view", {
|
||||
props: ["v-bandwidth-limit"],
|
||||
data: function () {
|
||||
return {
|
||||
config: this.vBandwidthLimit
|
||||
}
|
||||
},
|
||||
template: `<div>
|
||||
<div v-if="config.isOn">
|
||||
<span v-if="config.dailySize != null && config.dailySize.count > 0">日带宽限制:{{config.dailySize.count}}{{config.dailySize.unit.toUpperCase()}}<br/></span>
|
||||
<span v-if="config.monthlySize != null && config.monthlySize.count > 0">月带宽限制:{{config.monthlySize.count}}{{config.monthlySize.unit.toUpperCase()}}<br/></span>
|
||||
</div>
|
||||
<span v-else class="disabled">没有限制。</span>
|
||||
</div>`
|
||||
})
|
||||
@@ -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);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<menu-item :href="'/servers/certs/acme?type=available&keyword=' + keyword" :active="type == 'available'">有效证书({{countAvailable}})</menu-item>
|
||||
<menu-item :href="'/servers/certs/acme?type=expired&keyword=' + keyword" :active="type == 'expired'">过期证书<span :class="{red: countExpired > 0}">({{countExpired}})</span></menu-item>
|
||||
<menu-item :href="'/servers/certs/acme?type=7days&keyword=' + keyword" :active="type == '7days'">7天内过期<span :class="{red: count7Days > 0}">({{count7Days}})</span></menu-item>
|
||||
<menu-item :href="'/servers/certs/acme?type=30days&keyword=' + keyword" :active="type == '30days'">30天过期({{count30Days}})</menu-item>
|
||||
<menu-item :href="'/servers/certs/acme?type=30days&keyword=' + keyword" :active="type == '30days'">30天内过期({{count30Days}})</menu-item>
|
||||
<div class="item right">
|
||||
<form class="ui form">
|
||||
<div class="ui fields inline">
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<menu-item :href="'/servers/certs?type=available&keyword=' + keyword" :active="type == 'available'">有效证书({{countAvailable}})</menu-item>
|
||||
<menu-item :href="'/servers/certs?type=expired&keyword=' + keyword" :active="type == 'expired'">过期证书<span :class="{red: countExpired > 0}">({{countExpired}})</span></menu-item>
|
||||
<menu-item :href="'/servers/certs?type=7days&keyword=' + keyword" :active="type == '7days'">7天内过期<span :class="{red: count7Days > 0}">({{count7Days}})</span></menu-item>
|
||||
<menu-item :href="'/servers/certs?type=30days&keyword=' + keyword" :active="type == '30days'">30天过期({{count30Days}})</menu-item>
|
||||
<span class="item">|</span>
|
||||
<menu-item :href="'/servers/certs?type=30days&keyword=' + keyword" :active="type == '30days'">30天内过期({{count30Days}})</menu-item>
|
||||
<span class="item disabled">|</span>
|
||||
<a href="" class="item" @click.prevent="uploadCert">[上传证书]</a>
|
||||
</second-menu>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user