webui: Make some user preferences configurable

This extends 09f0237560.

- Extend user_pref with configurable profile fields and automatic migration defaults
- Move user info responses to read from user_pref instead of hardcoded values
- Add admin profile page for editing UI values after login

Signed-off-by: Sean Du <do4suki@gmail.com>
This commit is contained in:
2026-06-04 19:31:27 +08:00
parent 9ad66c2dc6
commit fa2efe4298
14 changed files with 419 additions and 46 deletions
+7 -7
View File
@@ -5,6 +5,7 @@ import (
usermodel "honoka-chan/internal/model/user" usermodel "honoka-chan/internal/model/user"
profileapischema "honoka-chan/internal/schema/api/profile" profileapischema "honoka-chan/internal/schema/api/profile"
"honoka-chan/internal/session" "honoka-chan/internal/session"
"strconv"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -12,11 +13,10 @@ import (
func profileInfo(ctx *gin.Context) (res any, err error) { func profileInfo(ctx *gin.Context) (res any, err error) {
ss := session.Get(ctx) ss := session.Get(ctx)
pref := ss.UserPref
pref := usermodel.UserPref{} displayUserID := pref.UserID
_, err = ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref) if inviteUserID, convErr := strconv.Atoi(pref.InviteCode); convErr == nil && inviteUserID > 0 {
if err != nil { displayUserID = inviteUserID
return nil, err
} }
unitCount, err := ss.UserEng.Table(new(usermodel.UserUnitData)). unitCount, err := ss.UserEng.Table(new(usermodel.UserUnitData)).
@@ -64,12 +64,12 @@ func profileInfo(ctx *gin.Context) (res any, err error) {
res = profileapischema.InfoResp{ res = profileapischema.InfoResp{
Result: profileapischema.InfoData{ Result: profileapischema.InfoData{
UserInfo: profileapischema.UserInfo{ UserInfo: profileapischema.UserInfo{
UserID: pref.UserID, UserID: displayUserID,
Name: pref.UserName, Name: pref.UserName,
Level: pref.UserLevel, Level: pref.UserLevel,
CostMax: 100, CostMax: 100,
UnitMax: 5000, UnitMax: 5000,
EnergyMax: 417, EnergyMax: pref.EffectiveEnergyMax(),
FriendMax: 99, FriendMax: 99,
UnitCnt: int(unitCount), UnitCnt: int(unitCount),
InviteCode: pref.InviteCode, InviteCode: pref.InviteCode,
-7
View File
@@ -1,7 +1,6 @@
package user package user
import ( import (
usermodel "honoka-chan/internal/model/user"
userapischema "honoka-chan/internal/schema/api/user" userapischema "honoka-chan/internal/schema/api/user"
"honoka-chan/internal/session" "honoka-chan/internal/session"
"time" "time"
@@ -12,12 +11,6 @@ import (
func userInfo(ctx *gin.Context) (res any, err error) { func userInfo(ctx *gin.Context) (res any, err error) {
ss := session.Get(ctx) ss := session.Get(ctx)
pref := usermodel.UserPref{}
_, err = ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref)
if err != nil {
return nil, err
}
res = userapischema.InfoResp{ res = userapischema.InfoResp{
Result: ss.GetUserInfo(), Result: ss.GetUserInfo(),
Status: 200, Status: 200,
+8 -1
View File
@@ -269,9 +269,16 @@ func addUser(dbSession *xorm.Session, phone, password string, isDefault bool) (g
BackgroundID: backgroundID, BackgroundID: backgroundID,
UnitOwningUserID: unitOwningUserID, UnitOwningUserID: unitOwningUserID,
UserName: userName, UserName: userName,
UserLevel: 1028, UserLevel: usermodel.DefaultUserLevel,
UserDesc: userDesc, UserDesc: userDesc,
InviteCode: strconv.Itoa(userID), InviteCode: strconv.Itoa(userID),
UserExp: usermodel.DefaultUserExp,
NextExp: usermodel.DefaultUserNextExp,
GameCoin: usermodel.DefaultUserGameCoin,
SnsCoin: usermodel.DefaultUserSnsCoin,
EnergyMax: usermodel.DefaultUserEnergyMax,
OverMaxEnergy: usermodel.DefaultUserOverMaxEnergy,
ProfileVersion: usermodel.CurrentUserPrefProfileVersion,
UpdateTime: time.Now().Unix(), UpdateTime: time.Now().Unix(),
} }
_, err = dbSession.Table("user_pref").Insert(&userPref) _, err = dbSession.Table("user_pref").Insert(&userPref)
+2 -2
View File
@@ -16,8 +16,8 @@ func continuee(ctx *gin.Context) {
ss.Respond(liveschema.ContinueResp{ ss.Respond(liveschema.ContinueResp{
ResponseData: liveschema.ContinueData{ ResponseData: liveschema.ContinueData{
BeforeSnsCoin: 100000, BeforeSnsCoin: ss.UserPref.SnsCoin,
AfterSnsCoin: 100000, AfterSnsCoin: ss.UserPref.SnsCoin,
ServerTimestamp: time.Now().Unix(), ServerTimestamp: time.Now().Unix(),
}, },
ReleaseInfo: []any{}, ReleaseInfo: []any{},
+4 -10
View File
@@ -42,12 +42,6 @@ func reward(ctx *gin.Context) {
return return
} }
pref := usermodel.UserPref{}
_, err = ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref)
if ss.CheckErr(err) {
return
}
totalScore := playRewardReq.ScoreSmile + playRewardReq.ScoreCool + playRewardReq.ScoreCute totalScore := playRewardReq.ScoreSmile + playRewardReq.ScoreCool + playRewardReq.ScoreCute
playResp := liveschema.RewardResp{ playResp := liveschema.RewardResp{
ResponseData: liveschema.RewardData{ ResponseData: liveschema.RewardData{
@@ -73,8 +67,8 @@ func reward(ctx *gin.Context) {
After: 99, After: 99,
}, },
PlayerExpLpMax: liveschema.PlayerExpLpMax{ PlayerExpLpMax: liveschema.PlayerExpLpMax{
Before: 417, Before: ss.UserPref.EffectiveEnergyMax(),
After: 417, After: ss.UserPref.EffectiveEnergyMax(),
}, },
GameCoin: 0, GameCoin: 0,
GameCoinRewardBoxFlag: false, GameCoinRewardBoxFlag: false,
@@ -95,8 +89,8 @@ func reward(ctx *gin.Context) {
AfterUserInfo: ss.GetUserInfo(), AfterUserInfo: ss.GetUserInfo(),
NextLevelInfo: []liveschema.NextLevelInfo{ NextLevelInfo: []liveschema.NextLevelInfo{
{ {
Level: pref.UserLevel, Level: ss.UserPref.UserLevel,
FromExp: 1089696, FromExp: ss.UserPref.UserExp,
}, },
}, },
GoalAccompInfo: liveschema.GoalAccompInfo{ GoalAccompInfo: liveschema.GoalAccompInfo{
-7
View File
@@ -2,7 +2,6 @@ package user
import ( import (
"honoka-chan/internal/middleware" "honoka-chan/internal/middleware"
usermodel "honoka-chan/internal/model/user"
"honoka-chan/internal/router" "honoka-chan/internal/router"
userschema "honoka-chan/internal/schema/user" userschema "honoka-chan/internal/schema/user"
"honoka-chan/internal/session" "honoka-chan/internal/session"
@@ -15,12 +14,6 @@ func userInfo(ctx *gin.Context) {
ss := session.Get(ctx) ss := session.Get(ctx)
defer ss.Finalize() defer ss.Finalize()
pref := usermodel.UserPref{}
_, err := ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref)
if ss.CheckErr(err) {
return
}
userResp := userschema.UserInfoResp{ userResp := userschema.UserInfoResp{
ResponseData: userschema.UserInfoData{ ResponseData: userschema.UserInfoData{
User: ss.GetUserInfo(), User: ss.GetUserInfo(),
+141
View File
@@ -0,0 +1,141 @@
package webui
import (
"honoka-chan/internal/middleware"
usermodel "honoka-chan/internal/model/user"
"honoka-chan/internal/router"
webuischema "honoka-chan/internal/schema/webui"
"honoka-chan/pkg/db"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
func profilePage(ctx *gin.Context) {
pref := usermodel.UserPref{}
exists, err := db.UserEng.Table(new(usermodel.UserPref)).
Where("user_id = ?", ctx.GetInt("userid")).Get(&pref)
if err != nil || !exists {
ctx.String(http.StatusInternalServerError, "用户资料读取失败")
return
}
ctx.HTML(http.StatusOK, "admin/profile.html", gin.H{
"menu": 2,
"url": strings.Split(ctx.Request.URL.String(), "?")[0],
"pref": pref,
})
}
func updateProfile(ctx *gin.Context) {
parseInt := func(key string, min int) (int, error) {
value := strings.TrimSpace(ctx.PostForm(key))
number, err := strconv.Atoi(value)
if err != nil {
return 0, err
}
if number < min {
return 0, strconv.ErrSyntax
}
return number, nil
}
userName := strings.TrimSpace(ctx.PostForm("user_name"))
inviteCode := strings.TrimSpace(ctx.PostForm("invite_code"))
userDesc := strings.TrimSpace(ctx.PostForm("user_desc"))
if userName == "" || inviteCode == "" {
ctx.JSON(http.StatusOK, webuischema.Msg{
Code: 1,
Message: "用户名和邀请码不能为空!",
})
return
}
userLevel, err := parseInt("user_level", 1)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "等级格式有误!"})
return
}
userExp, err := parseInt("user_exp", 0)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "当前经验格式有误!"})
return
}
nextExp, err := parseInt("next_exp", 0)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "升级经验格式有误!"})
return
}
gameCoin, err := parseInt("game_coin", 0)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "金币格式有误!"})
return
}
snsCoin, err := parseInt("sns_coin", 0)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "爱心格式有误!"})
return
}
energyMax, err := parseInt("energy_max", 1)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "体力上限格式有误!"})
return
}
overMaxEnergy, err := parseInt("over_max_energy", 0)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "当前体力格式有误!"})
return
}
pref := usermodel.UserPref{
UserName: userName,
UserLevel: userLevel,
UserDesc: userDesc,
InviteCode: inviteCode,
UserExp: userExp,
NextExp: nextExp,
GameCoin: gameCoin,
SnsCoin: snsCoin,
EnergyMax: energyMax,
OverMaxEnergy: overMaxEnergy,
ProfileVersion: usermodel.CurrentUserPrefProfileVersion,
UpdateTime: time.Now().Unix(),
}
_, err = db.UserEng.Table(new(usermodel.UserPref)).
Where("user_id = ?", ctx.GetInt("userid")).
Cols(
"user_name",
"user_level",
"user_desc",
"invite_code",
"user_exp",
"next_exp",
"game_coin",
"sns_coin",
"energy_max",
"over_max_energy",
"profile_version",
"update_time",
).
Update(&pref)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{
Code: 1,
Message: "保存失败!",
})
return
}
ctx.JSON(http.StatusOK, webuischema.Msg{
Code: 0,
Message: "保存成功!",
})
}
func init() {
router.AddHandler("admin", "GET", "/profile", middleware.WebAuth, profilePage)
router.AddHandler("admin", "POST", "/profile", middleware.WebAuth, updateProfile)
}
+84
View File
@@ -1,5 +1,22 @@
package usermodel package usermodel
import (
"strconv"
"strings"
)
const CurrentUserPrefProfileVersion = 1
const (
DefaultUserLevel = 1028
DefaultUserExp = 1089696
DefaultUserNextExp = 1207185
DefaultUserGameCoin = 112124104
DefaultUserSnsCoin = 10000
DefaultUserEnergyMax = 417
DefaultUserOverMaxEnergy = 0
)
type UserPref struct { type UserPref struct {
ID int `xorm:"id pk autoincr"` ID int `xorm:"id pk autoincr"`
UserID int `xorm:"user_id"` UserID int `xorm:"user_id"`
@@ -10,9 +27,76 @@ type UserPref struct {
UserLevel int `xorm:"user_level"` UserLevel int `xorm:"user_level"`
UserDesc string `xorm:"user_desc"` UserDesc string `xorm:"user_desc"`
InviteCode string `xorm:"invite_code"` InviteCode string `xorm:"invite_code"`
UserExp int `xorm:"user_exp"`
NextExp int `xorm:"next_exp"`
GameCoin int `xorm:"game_coin"`
SnsCoin int `xorm:"sns_coin"`
EnergyMax int `xorm:"energy_max"`
OverMaxEnergy int `xorm:"over_max_energy"`
ProfileVersion int `xorm:"profile_version"`
UpdateTime int64 `xorm:"update_time"` UpdateTime int64 `xorm:"update_time"`
} }
func (u *UserPref) ApplyProfileDefaults() {
if u.UserLevel <= 0 {
u.UserLevel = DefaultUserLevel
}
if strings.TrimSpace(u.InviteCode) == "" && u.UserID > 0 {
u.InviteCode = strconv.Itoa(u.UserID)
}
if u.UserExp < 0 || u.UserExp == 0 {
u.UserExp = DefaultUserExp
}
if u.NextExp < 0 || u.NextExp == 0 {
u.NextExp = DefaultUserNextExp
}
if u.GameCoin < 0 || u.GameCoin == 0 {
u.GameCoin = DefaultUserGameCoin
}
if u.SnsCoin < 0 || u.SnsCoin == 0 {
u.SnsCoin = DefaultUserSnsCoin
}
if u.EnergyMax <= 0 {
u.EnergyMax = DefaultUserEnergyMax
}
if u.OverMaxEnergy < 0 {
u.OverMaxEnergy = DefaultUserOverMaxEnergy
}
u.ProfileVersion = CurrentUserPrefProfileVersion
}
func (u UserPref) NeedsProfileMigration() bool {
return u.ProfileVersion < CurrentUserPrefProfileVersion
}
func (u UserPref) EffectiveEnergyMax() int {
if u.EnergyMax > 0 {
return u.EnergyMax
}
return DefaultUserEnergyMax
}
func (u UserPref) EffectiveCurrentEnergy() int {
if u.OverMaxEnergy > 0 {
return u.OverMaxEnergy
}
return u.EffectiveEnergyMax()
}
func UserPrefProfileColumns() []string {
return []string{
"user_level",
"invite_code",
"user_exp",
"next_exp",
"game_coin",
"sns_coin",
"energy_max",
"over_max_energy",
"profile_version",
}
}
func (UserPref) TableName() string { func (UserPref) TableName() string {
return "user_pref" return "user_pref"
} }
+19 -9
View File
@@ -18,6 +18,16 @@ func (ss *Session) GetUserPref(userID string) usermodel.UserPref {
ss.Abort(errors.New("用户不存在!")) ss.Abort(errors.New("用户不存在!"))
return usermodel.UserPref{} return usermodel.UserPref{}
} }
if pref.NeedsProfileMigration() {
pref.ApplyProfileDefaults()
_, err = ss.UserEng.Table(new(usermodel.UserPref)).
Where("user_id = ?", userID).
Cols(usermodel.UserPrefProfileColumns()...).
Update(&pref)
if ss.CheckErr(err) {
return usermodel.UserPref{}
}
}
return pref return pref
} }
@@ -27,22 +37,22 @@ func (ss *Session) GetUserInfo() userschema.UserInfo {
UserID: ss.UserID, UserID: ss.UserID,
Name: ss.UserPref.UserName, Name: ss.UserPref.UserName,
Level: ss.UserPref.UserLevel, Level: ss.UserPref.UserLevel,
Exp: 1089696, Exp: ss.UserPref.UserExp,
PreviousExp: 0, PreviousExp: 0,
NextExp: 1207185, NextExp: ss.UserPref.NextExp,
GameCoin: 112124104, GameCoin: ss.UserPref.GameCoin,
SnsCoin: 10000, SnsCoin: ss.UserPref.SnsCoin,
FreeSnsCoin: 50000, FreeSnsCoin: ss.UserPref.SnsCoin,
PaidSnsCoin: 50000, PaidSnsCoin: 0,
SocialPoint: 1438165, SocialPoint: 1438165,
UnitMax: 5000, UnitMax: 5000,
WaitingUnitMax: 5000, WaitingUnitMax: 5000,
CurrentEnergy: 417, CurrentEnergy: ss.UserPref.EffectiveCurrentEnergy(),
EnergyMax: 417, EnergyMax: ss.UserPref.EffectiveEnergyMax(),
EnergyFullTime: "2023-03-20 01:28:55", EnergyFullTime: "2023-03-20 01:28:55",
LicenseLiveEnergyRecoverlyTime: 60, LicenseLiveEnergyRecoverlyTime: 60,
EnergyFullNeedTime: 0, EnergyFullNeedTime: 0,
OverMaxEnergy: 417, OverMaxEnergy: ss.UserPref.EffectiveCurrentEnergy(),
TrainingEnergy: 10, TrainingEnergy: 10,
TrainingEnergyMax: 10, TrainingEnergyMax: 10,
FriendMax: 99, FriendMax: 99,
+27
View File
@@ -31,6 +31,33 @@ func CreateTables() {
db.UserEng.Sync2(new(usermodel.Users)) db.UserEng.Sync2(new(usermodel.Users))
db.UserEng.Sync2(new(usermodel.UserUnit)) db.UserEng.Sync2(new(usermodel.UserUnit))
db.UserEng.Sync2(new(usermodel.UserUnitSkillEquip)) db.UserEng.Sync2(new(usermodel.UserUnitSkillEquip))
MigrateUserPref()
}
func MigrateUserPref() {
session := db.UserEng.NewSession()
defer session.Close()
prefList := []usermodel.UserPref{}
err := session.Table(new(usermodel.UserPref)).
Where("profile_version < ?", usermodel.CurrentUserPrefProfileVersion).
Or("profile_version IS NULL").
Find(&prefList)
if err != nil {
log.Fatalln("迁移 user_pref 失败:", err.Error())
}
for _, pref := range prefList {
pref.ApplyProfileDefaults()
_, err = session.Table(new(usermodel.UserPref)).
ID(pref.ID).
Cols(usermodel.UserPrefProfileColumns()...).
Update(&pref)
if err != nil {
log.Fatalln("迁移 user_pref 失败:", err.Error())
}
}
} }
func LoadUnitData() { func LoadUnitData() {
+1 -1
View File
@@ -1,5 +1,5 @@
{{ define "admin/nav/footer.html" }} {{ define "admin/nav/footer.html" }}
<div class="layui-footer"> <div class="layui-footer">
Copyright © 2023 YumeMichi Copyright © 2023-2026 YumeMichi
</div> </div>
{{ end }} {{ end }}
+1 -1
View File
@@ -14,7 +14,7 @@
设置 设置
</a> </a>
<dl class="layui-nav-child"> <dl class="layui-nav-child">
<dd><a href="">个人信息</a></dd> <dd><a href="/admin/profile">个人信息</a></dd>
<dd><a href="/admin/logout">登出</a></dd> <dd><a href="/admin/logout">登出</a></dd>
</dl> </dl>
</li> </li>
+7 -1
View File
@@ -2,6 +2,12 @@
<div class="layui-side"> <div class="layui-side">
<div class="layui-side-scroll"> <div class="layui-side-scroll">
<ul class="layui-nav layui-nav-tree" lay-filter="test"> <ul class="layui-nav layui-nav-tree" lay-filter="test">
<li class="layui-nav-item {{ if eq .menu 2 }} layui-nav-itemed {{ end }}">
<a class="" href="javascript:;">用户设置</a>
<dl class="layui-nav-child">
<dd {{ if eq .url "/admin/profile" }} class="layui-this" {{ end }}><a href="/admin/profile">个人资料</a></dd>
</dl>
</li>
<li class="layui-nav-item {{ if eq .menu 1 }} layui-nav-itemed {{ end }}"> <li class="layui-nav-item {{ if eq .menu 1 }} layui-nav-itemed {{ end }}">
<a class="" href="javascript:;">卡片管理</a> <a class="" href="javascript:;">卡片管理</a>
<dl class="layui-nav-child"> <dl class="layui-nav-child">
@@ -14,4 +20,4 @@
</ul> </ul>
</div> </div>
</div> </div>
{{ end }} {{ end }}
+118
View File
@@ -0,0 +1,118 @@
{{ define "admin/profile.html" }}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>SIF 小助手</title>
{{ template "admin/nav/css.html" . }}
</head>
<body>
<div class="layui-layout layui-layout-admin">
{{ template "admin/nav/header.html" . }}
{{ template "admin/nav/side.html" . }}
<div class="layui-body">
<form class="layui-form layui-col-md8" action="" style="padding: 15px 20px 30px;">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="user_name" required lay-verify="required" autocomplete="off"
class="layui-input" value="{{ .pref.UserName }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">等级</label>
<div class="layui-input-block">
<input type="number" name="user_level" min="1" required lay-verify="required" autocomplete="off"
class="layui-input" value="{{ .pref.UserLevel }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">当前经验</label>
<div class="layui-input-block">
<input type="number" name="user_exp" min="0" required lay-verify="required" autocomplete="off"
class="layui-input" value="{{ .pref.UserExp }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">升级经验</label>
<div class="layui-input-block">
<input type="number" name="next_exp" min="0" required lay-verify="required" autocomplete="off"
class="layui-input" value="{{ .pref.NextExp }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">金币</label>
<div class="layui-input-block">
<input type="number" name="game_coin" min="0" required lay-verify="required" autocomplete="off"
class="layui-input" value="{{ .pref.GameCoin }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">爱心</label>
<div class="layui-input-block">
<input type="number" name="sns_coin" min="0" required lay-verify="required" autocomplete="off"
class="layui-input" value="{{ .pref.SnsCoin }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">体力上限</label>
<div class="layui-input-block">
<input type="number" name="energy_max" min="1" required lay-verify="required" autocomplete="off"
class="layui-input" value="{{ .pref.EnergyMax }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">当前体力</label>
<div class="layui-input-block">
<input type="number" name="over_max_energy" min="0" required lay-verify="required"
autocomplete="off" class="layui-input" value="{{ .pref.OverMaxEnergy }}">
<div class="layui-form-mid layui-word-aux">填 0 时按体力上限返回给客户端。</div>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">用户 ID</label>
<div class="layui-input-block">
<input type="text" name="invite_code" required lay-verify="required" autocomplete="off"
class="layui-input" value="{{ .pref.InviteCode }}">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">简介</label>
<div class="layui-input-block">
<textarea name="user_desc" class="layui-textarea"
style="min-height: 120px;">{{ .pref.UserDesc }}</textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button type="button" class="layui-btn" id="save-profile">保存</button>
</div>
</div>
</form>
</div>
{{ template "admin/nav/footer.html" . }}
</div>
<script src="/static/js/layui.js"></script>
<script>
layui.use(['form', 'layer'], function () {
var $ = layui.jquery
, layer = layui.layer;
$("#save-profile").click(function () {
$.post("/admin/profile", $("form").serialize(), function (res) {
layer.msg(res.message || "请求完成");
}).fail(function () {
layer.msg("请求失败,请稍后再试");
});
});
});
</script>
</body>
</html>
{{ end }}