增加租户官网模块
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
package tagengine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RenderCtx 渲染上下文,携带租户与当前路由信息
|
||||
type RenderCtx struct {
|
||||
Tid uint64 // 租户 ID
|
||||
ArticleID uint64 // 文章详情页的文章 ID
|
||||
PagePath string // 单页路由的 path
|
||||
Page int // 列表分页页码
|
||||
PageSize int // 列表分页大小(pagelist 与列表标签保持一致)
|
||||
}
|
||||
|
||||
// Provider 循环标签数据提供器:返回若干行数据,每行为 字段名->值
|
||||
type Provider func(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error)
|
||||
|
||||
// SelfCloseHandler 自闭合标签处理器:直接输出 HTML 片段
|
||||
type SelfCloseHandler func(tid uint64, params map[string]string, ctx *RenderCtx) (string, error)
|
||||
|
||||
// GlobalProvider 全局标签提供器:返回 标签名->值 映射
|
||||
type GlobalProvider func(tid uint64, ctx *RenderCtx) (map[string]string, error)
|
||||
|
||||
var (
|
||||
blockProviders = map[string]Provider{}
|
||||
selfCloseHandlers = map[string]SelfCloseHandler{}
|
||||
globalProvider GlobalProvider
|
||||
)
|
||||
|
||||
// RegisterBlock 注册循环标签({yz:name}...{/yz:name})
|
||||
func RegisterBlock(name string, p Provider) { blockProviders[name] = p }
|
||||
|
||||
// RegisterSelfClose 注册自闭合标签({yz:name ... /})
|
||||
func RegisterSelfClose(name string, h SelfCloseHandler) { selfCloseHandlers[name] = h }
|
||||
|
||||
// RegisterGlobal 注册全局标签提供器({yz:xxx} 直接替换)
|
||||
func RegisterGlobal(p GlobalProvider) { globalProvider = p }
|
||||
|
||||
var (
|
||||
// 最内层块标签(内部不再嵌套块标签),反复匹配直至收敛
|
||||
reBlock = regexp.MustCompile(`(?s)\{yz:([a-zA-Z]+)((?:\s+[a-zA-Z_][\w-]*\s*=\s*"[^"]*")*)\s*\}((?:(?!\{/?yz:).)*?)\{/yz:\1\}`)
|
||||
// 自闭合标签 {yz:name attr="v" /}
|
||||
reSelfClose = regexp.MustCompile(`\{yz:([a-zA-Z]+)((?:\s+[a-zA-Z_][\w-]*\s*=\s*"[^"]*")*)\s*/\}`)
|
||||
// 全局标签 {yz:word}
|
||||
reGlobal = regexp.MustCompile(`\{yz:([a-zA-Z_]\w*)\}`)
|
||||
// 属性解析 key="value"
|
||||
reAttr = regexp.MustCompile(`([a-zA-Z_][\w-]*)\s*=\s*"([^"]*)"`)
|
||||
// 循环体内字段占位 [field:name/]
|
||||
reField = regexp.MustCompile(`\[field:([a-zA-Z_]\w*)/\]`)
|
||||
// include 文件名校验,仅允许简单文件名
|
||||
reIncludeFile = regexp.MustCompile(`^[a-zA-Z0-9_\-\.]+$`)
|
||||
)
|
||||
|
||||
// parseAttrs 解析标签属性串为 map
|
||||
func parseAttrs(attrStr string) map[string]string {
|
||||
out := map[string]string{}
|
||||
for _, m := range reAttr.FindAllStringSubmatch(attrStr, -1) {
|
||||
out[strings.ToLower(m[1])] = m[2]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Render 渲染模板目录下的指定文件
|
||||
// 流水线:include 展开 → 块标签 → 自闭合标签 → 全局标签
|
||||
func Render(templateDir, file string, ctx *RenderCtx) (string, error) {
|
||||
content, err := os.ReadFile(filepath.Join(templateDir, file))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("模板文件不存在: %s", file)
|
||||
}
|
||||
out := string(content)
|
||||
|
||||
out, err = expandIncludes(templateDir, out, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out, err = renderBlocks(out, ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out, err = renderSelfClose(out, ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out = renderGlobals(out, ctx)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// expandIncludes 递归展开 {yz:include file="xxx"/},限制嵌套深度防止死循环
|
||||
func expandIncludes(dir, content string, depth int) (string, error) {
|
||||
if depth > 5 {
|
||||
return content, fmt.Errorf("include 嵌套层级过深")
|
||||
}
|
||||
var ferr error
|
||||
out := reSelfClose.ReplaceAllStringFunc(content, func(m string) string {
|
||||
sub := reSelfClose.FindStringSubmatch(m)
|
||||
if sub[1] != "include" {
|
||||
return m
|
||||
}
|
||||
attrs := parseAttrs(sub[2])
|
||||
f := strings.TrimSpace(attrs["file"])
|
||||
if f == "" || !reIncludeFile.MatchString(f) {
|
||||
return ""
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(dir, f))
|
||||
if err != nil {
|
||||
ferr = fmt.Errorf("include 文件不存在: %s", f)
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
})
|
||||
if ferr != nil {
|
||||
return "", ferr
|
||||
}
|
||||
if strings.Contains(out, "{yz:include") {
|
||||
return expandIncludes(dir, out, depth+1)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// renderBlocks 反复解析最内层块标签直至全部展开
|
||||
func renderBlocks(content string, ctx *RenderCtx) (string, error) {
|
||||
out := content
|
||||
for i := 0; i < 10; i++ {
|
||||
if !reBlock.MatchString(out) {
|
||||
break
|
||||
}
|
||||
var ferr error
|
||||
out = reBlock.ReplaceAllStringFunc(out, func(m string) string {
|
||||
sub := reBlock.FindStringSubmatch(m)
|
||||
name := strings.ToLower(sub[1])
|
||||
attrs := parseAttrs(sub[2])
|
||||
body := sub[3]
|
||||
p, ok := blockProviders[name]
|
||||
if !ok {
|
||||
return "" // 未注册的块标签直接置空,避免标签泄漏到前台
|
||||
}
|
||||
rows, err := p(ctx.Tid, attrs, ctx)
|
||||
if err != nil {
|
||||
ferr = err
|
||||
return ""
|
||||
}
|
||||
var sb strings.Builder
|
||||
for _, row := range rows {
|
||||
sb.WriteString(reField.ReplaceAllStringFunc(body, func(fm string) string {
|
||||
field := reField.FindStringSubmatch(fm)[1]
|
||||
return row[field]
|
||||
}))
|
||||
}
|
||||
return sb.String()
|
||||
})
|
||||
if ferr != nil {
|
||||
return "", ferr
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// renderSelfClose 处理剩余自闭合标签(onepage/pagelist 等,include 已先行展开)
|
||||
func renderSelfClose(content string, ctx *RenderCtx) (string, error) {
|
||||
var ferr error
|
||||
out := reSelfClose.ReplaceAllStringFunc(content, func(m string) string {
|
||||
sub := reSelfClose.FindStringSubmatch(m)
|
||||
name := strings.ToLower(sub[1])
|
||||
h, ok := selfCloseHandlers[name]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
s, err := h(ctx.Tid, parseAttrs(sub[2]), ctx)
|
||||
if err != nil {
|
||||
ferr = err
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
})
|
||||
if ferr != nil {
|
||||
return "", ferr
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// renderGlobals 替换全局标签;未提供的标签置空
|
||||
func renderGlobals(content string, ctx *RenderCtx) string {
|
||||
var vals map[string]string
|
||||
if globalProvider != nil {
|
||||
vals, _ = globalProvider(ctx.Tid, ctx)
|
||||
}
|
||||
return reGlobal.ReplaceAllStringFunc(content, func(m string) string {
|
||||
key := reGlobal.FindStringSubmatch(m)[1]
|
||||
if vals != nil {
|
||||
if v, ok := vals[key]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package tagengine
|
||||
|
||||
// TagMeta 标签元信息:由注册表统一维护,供 platform 端"标签调用说明"页输出。
|
||||
// 新增标签时在此登记一条,文档页自动同步,避免文档与实现脱节。
|
||||
type TagMeta struct {
|
||||
Name string `json:"name"` // 标签名
|
||||
Category string `json:"category"` // 分类:global 全局 / loop 循环 / single 单页 / other 其他
|
||||
Syntax string `json:"syntax"` // 语法示例
|
||||
Desc string `json:"desc"` // 用途说明
|
||||
Params []string `json:"params"` // 可用参数(key=说明)
|
||||
Fields []string `json:"fields"` // 循环体内可用 [field:xxx/] 字段
|
||||
Example string `json:"example"` // 模板内示例代码
|
||||
}
|
||||
|
||||
// TagDocs 返回全部已登记标签的说明数据
|
||||
func TagDocs() []TagMeta {
|
||||
return []TagMeta{
|
||||
{
|
||||
Name: "全局站点信息",
|
||||
Category: "global",
|
||||
Syntax: "{yz:sitename} {yz:logo} {yz:logow} {yz:ico} {yz:icp} {yz:copyright} {yz:companyname} {yz:description}",
|
||||
Desc: "取自当前租户的站点设置(backend 端 - 站点设置),直接替换为对应文本/地址,无需闭合。",
|
||||
Params: nil,
|
||||
Fields: nil,
|
||||
Example: "<title>{yz:sitename}</title>\n<img src=\"{yz:logo}\" alt=\"{yz:sitename}\" />\n<p>{yz:icp} {yz:copyright}</p>",
|
||||
},
|
||||
{
|
||||
Name: "nav 导航菜单",
|
||||
Category: "loop",
|
||||
Syntax: "{yz:nav}...{/yz:nav}",
|
||||
Desc: "循环输出当前租户在 backend 端配置的前台导航菜单(yz_backend_menu_front)。",
|
||||
Params: nil,
|
||||
Fields: []string{"id 菜单ID", "name 菜单名称", "url 跳转路径", "children 子菜单HTML(<ul class=\"yz-sub-menu\">结构,无子菜单时为空)"},
|
||||
Example: "{yz:nav}<li><a href=\"[field:url/]\">[field:name/]</a>[field:children/]</li>{/yz:nav}",
|
||||
},
|
||||
{
|
||||
Name: "banner 轮播图",
|
||||
Category: "loop",
|
||||
Syntax: "{yz:banner row=\"5\"}...{/yz:banner}",
|
||||
Desc: "循环输出当前租户启用中的 Banner(backend 端 - Banner管理),按 sort 升序。",
|
||||
Params: []string{"row 输出条数,默认 10"},
|
||||
Fields: []string{"id BannerID", "title 标题", "desc 简介", "image 图片地址", "url 跳转地址"},
|
||||
Example: "{yz:banner}<a href=\"[field:url/]\"><img src=\"[field:image/]\" alt=\"[field:title/]\" /></a>{/yz:banner}",
|
||||
},
|
||||
{
|
||||
Name: "arclist 文章列表",
|
||||
Category: "loop",
|
||||
Syntax: "{yz:arclist row=\"6\" cateid=\"2\" titlelen=\"30\" flag=\"recommend\"}...{/yz:arclist}",
|
||||
Desc: "循环输出当前租户已发布的文章,默认按 置顶>最新 排序。",
|
||||
Params: []string{"row 输出条数,默认 10,最大 50", "cateid 文章分类ID,不填为全部", "titlelen 标题截断字数,不填为完整标题", "flag recommend=推荐 / top=置顶 / hot=按阅读量排序"},
|
||||
Fields: []string{"id 文章ID", "title 标题(受 titlelen 影响)", "titlefull 完整标题", "desc 摘要", "image 缩略图", "arcurl 详情页链接(/news/ID)", "pubdate 发布日期", "views 阅读量", "author 作者"},
|
||||
Example: "{yz:arclist row=\"6\" titlelen=\"20\"}\n<li><a href=\"[field:arcurl/]\">[field:title/]</a><span>[field:pubdate/]</span></li>\n{/yz:arclist}",
|
||||
},
|
||||
{
|
||||
Name: "arcview 文章详情",
|
||||
Category: "single",
|
||||
Syntax: "{yz:arcview}...{/yz:arcview}",
|
||||
Desc: "仅在文章详情页(/news/:id)模板内有效,输出当前文章信息。",
|
||||
Params: nil,
|
||||
Fields: []string{"id 文章ID", "title 标题", "content 富文本正文", "desc 摘要", "image 缩略图", "author 作者", "pubdate 发布时间", "views 阅读量"},
|
||||
Example: "{yz:arcview}\n<h1>[field:title/]</h1>\n<p>[field:pubdate/] 阅读 [field:views/]</p>\n<div class=\"content\">[field:content/]</div>\n{/yz:arcview}",
|
||||
},
|
||||
{
|
||||
Name: "friendlink 友情链接",
|
||||
Category: "loop",
|
||||
Syntax: "{yz:friendlink}...{/yz:friendlink}",
|
||||
Desc: "循环输出当前租户启用中的友情链接(backend 端 - 友情链接),按 sort 升序。",
|
||||
Params: []string{"row 输出条数,默认 20"},
|
||||
Fields: []string{"id 链接ID", "name 链接名称", "url 链接地址", "logo Logo地址", "description 描述"},
|
||||
Example: "{yz:friendlink}<a href=\"[field:url/]\" target=\"_blank\">[field:name/]</a>{/yz:friendlink}",
|
||||
},
|
||||
{
|
||||
Name: "onepage 单页内容",
|
||||
Category: "single",
|
||||
Syntax: "{yz:onepage path=\"about\" field=\"content\"/}",
|
||||
Desc: "输出指定单页(backend 端 - 单页管理)的标题或正文;在单页路由(/page/:path)模板内可省略 path,自动取当前单页。",
|
||||
Params: []string{"path 单页路径标识,单页路由模板内可省略", "field 输出字段,content(默认) / title"},
|
||||
Fields: nil,
|
||||
Example: "{yz:onepage field=\"title\"/>\n<div class=\"content\">{yz:onepage/}</div>",
|
||||
},
|
||||
{
|
||||
Name: "pagelist 分页",
|
||||
Category: "other",
|
||||
Syntax: "{yz:pagelist/}",
|
||||
Desc: "在新闻列表页(/news)模板内输出分页链接(上一页/页码/下一页),链接指向 /news?page=N,样式类名 yz-pagination/yz-page。",
|
||||
Params: nil,
|
||||
Fields: nil,
|
||||
Example: "<div class=\"pager\">{yz:pagelist/}</div>",
|
||||
},
|
||||
{
|
||||
Name: "include 包含子模板",
|
||||
Category: "other",
|
||||
Syntax: "{yz:include file=\"header.html\"/}",
|
||||
Desc: "在当前模板位置引入同目录下的子模板文件(如公共头部/底部),最大嵌套 5 层。",
|
||||
Params: []string{"file 子模板文件名,如 header.html"},
|
||||
Fields: nil,
|
||||
Example: "{yz:include file=\"header.html\"/}",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package tagengine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
const (
|
||||
dateLayout = "2006-01-02"
|
||||
datetimeLayout = "2006-01-02 15:04"
|
||||
maxIncludeRows = 200
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterGlobal(globalSiteInfo)
|
||||
RegisterBlock("nav", navProvider)
|
||||
RegisterBlock("banner", bannerProvider)
|
||||
RegisterBlock("arclist", arclistProvider)
|
||||
RegisterBlock("arcview", arcviewProvider)
|
||||
RegisterBlock("friendlink", friendlinkProvider)
|
||||
RegisterSelfClose("onepage", onepageHandler)
|
||||
RegisterSelfClose("pagelist", pagelistHandler)
|
||||
}
|
||||
|
||||
func atoiDefault(s string, def int) int {
|
||||
if s == "" {
|
||||
return def
|
||||
}
|
||||
if n, err := strconv.Atoi(strings.TrimSpace(s)); err == nil {
|
||||
return n
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// truncateTitle 按字符数(而非字节)安全截断标题
|
||||
func truncateTitle(s string, n int) string {
|
||||
if n <= 0 {
|
||||
return s
|
||||
}
|
||||
rs := []rune(s)
|
||||
if len(rs) <= n {
|
||||
return s
|
||||
}
|
||||
return string(rs[:n]) + "..."
|
||||
}
|
||||
|
||||
// publishedCond 已发布状态条件:兼容历史 status=1 与现行 status=2(已发布)
|
||||
func publishedCond(qs orm.QuerySeter) orm.QuerySeter {
|
||||
return qs.Filter("status__in", []int8{1, 2})
|
||||
}
|
||||
|
||||
// globalSiteInfo 全局标签:站点设置各字段
|
||||
func globalSiteInfo(tid uint64, ctx *RenderCtx) (map[string]string, error) {
|
||||
out := map[string]string{
|
||||
"sitename": "", "logo": "", "logow": "", "ico": "", "icp": "",
|
||||
"copyright": "", "companyname": "", "description": "",
|
||||
}
|
||||
var row models.TenantSiteSetting
|
||||
err := models.Orm.QueryTable(new(models.TenantSiteSetting)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return out, nil
|
||||
}
|
||||
out["sitename"] = row.Sitename
|
||||
out["logo"] = row.Logo
|
||||
out["logow"] = row.Logow
|
||||
out["ico"] = row.Ico
|
||||
out["icp"] = row.Icp
|
||||
out["copyright"] = row.Copyright
|
||||
out["companyname"] = row.Companyname
|
||||
out["description"] = row.Description
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// navProvider 导航菜单:一级菜单循环,[field:children/] 输出子菜单 HTML
|
||||
func navProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
|
||||
var rows []models.BackendMenuFront
|
||||
_, err := models.Orm.QueryTable(new(models.BackendMenuFront)).
|
||||
Filter("tenant_id", tid).
|
||||
Filter("pid", 0).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").
|
||||
Limit(maxIncludeRows).
|
||||
All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var children []models.BackendMenuFront
|
||||
_, _ = models.Orm.QueryTable(new(models.BackendMenuFront)).
|
||||
Filter("tenant_id", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("pid__gt", 0).
|
||||
OrderBy("sort", "id").
|
||||
Limit(maxIncludeRows).
|
||||
All(&children)
|
||||
|
||||
childMap := map[int64][]models.BackendMenuFront{}
|
||||
for _, c := range children {
|
||||
childMap[c.Pid] = append(childMap[c.Pid], c)
|
||||
}
|
||||
|
||||
out := make([]map[string]string, 0, len(rows))
|
||||
for _, m := range rows {
|
||||
childHTML := ""
|
||||
if subs, ok := childMap[int64(m.ID)]; ok && len(subs) > 0 {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`<ul class="yz-sub-menu">`)
|
||||
for _, s := range subs {
|
||||
sb.WriteString(fmt.Sprintf(`<li><a href="%s">%s</a></li>`, s.Path, s.Title))
|
||||
}
|
||||
sb.WriteString(`</ul>`)
|
||||
childHTML = sb.String()
|
||||
}
|
||||
out = append(out, map[string]string{
|
||||
"id": strconv.FormatUint(m.ID, 10),
|
||||
"name": m.Title,
|
||||
"url": m.Path,
|
||||
"children": childHTML,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// bannerProvider 首页 Banner 循环
|
||||
func bannerProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
|
||||
row := atoiDefault(params["row"], 10)
|
||||
var rows []models.CmsFrontendBanner
|
||||
_, err := models.Orm.QueryTable(new(models.CmsFrontendBanner)).
|
||||
Filter("tid", tid).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").
|
||||
Limit(row).
|
||||
All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]map[string]string, 0, len(rows))
|
||||
for _, b := range rows {
|
||||
out = append(out, map[string]string{
|
||||
"id": strconv.FormatUint(b.ID, 10),
|
||||
"title": b.Title,
|
||||
"desc": b.Desc,
|
||||
"image": b.Image,
|
||||
"url": b.Url,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// friendlinkProvider 友情链接循环
|
||||
func friendlinkProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
|
||||
row := atoiDefault(params["row"], 20)
|
||||
var rows []models.CmsFrontendFriendlink
|
||||
_, err := models.Orm.QueryTable(new(models.CmsFrontendFriendlink)).
|
||||
Filter("tid", tid).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").
|
||||
Limit(row).
|
||||
All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]map[string]string, 0, len(rows))
|
||||
for _, f := range rows {
|
||||
out = append(out, map[string]string{
|
||||
"id": strconv.FormatUint(f.ID, 10),
|
||||
"name": f.LinkName,
|
||||
"url": f.LinkUrl,
|
||||
"logo": f.LinkLogo,
|
||||
"description": f.Description,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// arclistProvider 文章列表循环,支持 row/cateid/titlelen/flag 参数
|
||||
func arclistProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
|
||||
row := atoiDefault(params["row"], 10)
|
||||
if row <= 0 || row > 50 {
|
||||
row = 10
|
||||
}
|
||||
titleLen := atoiDefault(params["titlelen"], 0)
|
||||
|
||||
qs := publishedCond(models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true))
|
||||
if cid, err := strconv.ParseUint(strings.TrimSpace(params["cateid"]), 10, 64); err == nil && cid > 0 {
|
||||
qs = qs.Filter("cate_id", cid)
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(params["flag"])) {
|
||||
case "recommend":
|
||||
qs = qs.Filter("recommend", 1)
|
||||
case "top":
|
||||
qs = qs.Filter("top", 1)
|
||||
case "hot":
|
||||
qs = qs.OrderBy("-views", "-id")
|
||||
}
|
||||
|
||||
var rows []models.CmsArticle
|
||||
order := []string{"-top", "-id"}
|
||||
if strings.ToLower(strings.TrimSpace(params["flag"])) == "hot" {
|
||||
order = []string{"-views", "-id"}
|
||||
}
|
||||
// 列表页携带页码时按页偏移,配合 {yz:pagelist/} 分页
|
||||
offset := 0
|
||||
if ctx.Page > 1 {
|
||||
offset = (ctx.Page - 1) * row
|
||||
}
|
||||
_, err := qs.OrderBy(order...).Limit(row, offset).All(&rows)
|
||||
if err != nil && err != orm.ErrNoRows {
|
||||
return nil, err
|
||||
}
|
||||
return articleRows(rows, titleLen), nil
|
||||
}
|
||||
|
||||
// articleRows 文章行转标签字段
|
||||
func articleRows(rows []models.CmsArticle, titleLen int) []map[string]string {
|
||||
out := make([]map[string]string, 0, len(rows))
|
||||
for _, a := range rows {
|
||||
pub := ""
|
||||
if a.PublishTime != nil {
|
||||
pub = a.PublishTime.Format(dateLayout)
|
||||
} else {
|
||||
pub = a.CreateTime.Format(dateLayout)
|
||||
}
|
||||
title := a.Title
|
||||
if titleLen > 0 {
|
||||
title = truncateTitle(title, titleLen)
|
||||
}
|
||||
out = append(out, map[string]string{
|
||||
"id": strconv.FormatUint(a.ID, 10),
|
||||
"title": title,
|
||||
"titlefull": a.Title,
|
||||
"desc": a.Desc,
|
||||
"image": a.Image,
|
||||
"arcurl": "/news/" + strconv.FormatUint(a.ID, 10),
|
||||
"pubdate": pub,
|
||||
"views": strconv.Itoa(a.Views),
|
||||
"author": a.Author,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// arcviewProvider 文章详情(详情页模板内),取当前路由文章
|
||||
func arcviewProvider(tid uint64, params map[string]string, ctx *RenderCtx) ([]map[string]string, error) {
|
||||
if ctx.ArticleID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var a models.CmsArticle
|
||||
err := models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("id", ctx.ArticleID).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&a)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
pub := ""
|
||||
if a.PublishTime != nil {
|
||||
pub = a.PublishTime.Format(datetimeLayout)
|
||||
} else {
|
||||
pub = a.CreateTime.Format(datetimeLayout)
|
||||
}
|
||||
return []map[string]string{{
|
||||
"id": strconv.FormatUint(a.ID, 10),
|
||||
"title": a.Title,
|
||||
"content": a.Content,
|
||||
"desc": a.Desc,
|
||||
"image": a.Image,
|
||||
"author": a.Author,
|
||||
"pubdate": pub,
|
||||
"views": strconv.Itoa(a.Views),
|
||||
}}, nil
|
||||
}
|
||||
|
||||
// onepageHandler 单页内容 {yz:onepage path="about" field="content"/}
|
||||
func onepageHandler(tid uint64, params map[string]string, ctx *RenderCtx) (string, error) {
|
||||
path := strings.TrimSpace(params["path"])
|
||||
if path == "" {
|
||||
path = ctx.PagePath
|
||||
}
|
||||
if path == "" {
|
||||
return "", nil
|
||||
}
|
||||
field := strings.ToLower(strings.TrimSpace(params["field"]))
|
||||
if field == "" {
|
||||
field = "content"
|
||||
}
|
||||
|
||||
var row models.CmsFrontendOnepage
|
||||
err := models.Orm.QueryTable(new(models.CmsFrontendOnepage)).
|
||||
Filter("tid", tid).
|
||||
Filter("path", path).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
switch field {
|
||||
case "title":
|
||||
return row.Title, nil
|
||||
default:
|
||||
return row.Content, nil
|
||||
}
|
||||
}
|
||||
|
||||
// newsQueryBase 新闻列表统一查询条件(pagelist 与 news.html 保持一致)
|
||||
func newsQueryBase(tid uint64) orm.QuerySeter {
|
||||
return publishedCond(models.Orm.QueryTable(new(models.CmsArticle)).
|
||||
Filter("tid", tid).
|
||||
Filter("delete_time__isnull", true))
|
||||
}
|
||||
|
||||
// pagelistHandler 分页链接 {yz:pagelist/},配合 /news?page=N
|
||||
func pagelistHandler(tid uint64, params map[string]string, ctx *RenderCtx) (string, error) {
|
||||
pageSize := ctx.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
total, err := newsQueryBase(tid).Count()
|
||||
if err != nil || total == 0 {
|
||||
return "", nil
|
||||
}
|
||||
pages := int((total + int64(pageSize) - 1) / int64(pageSize))
|
||||
cur := ctx.Page
|
||||
if cur < 1 {
|
||||
cur = 1
|
||||
}
|
||||
if cur > pages {
|
||||
cur = pages
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`<nav class="yz-pagination">`)
|
||||
if cur > 1 {
|
||||
sb.WriteString(fmt.Sprintf(`<a class="yz-page prev" href="/news?page=%d">上一页</a>`, cur-1))
|
||||
}
|
||||
start := cur - 3
|
||||
if start < 1 {
|
||||
start = 1
|
||||
}
|
||||
end := cur + 3
|
||||
if end > pages {
|
||||
end = pages
|
||||
}
|
||||
for i := start; i <= end; i++ {
|
||||
if i == cur {
|
||||
sb.WriteString(fmt.Sprintf(`<span class="yz-page current">%d</span>`, i))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(`<a class="yz-page" href="/news?page=%d">%d</a>`, i, i))
|
||||
}
|
||||
}
|
||||
if cur < pages {
|
||||
sb.WriteString(fmt.Sprintf(`<a class="yz-page next" href="/news?page=%d">下一页</a>`, cur+1))
|
||||
}
|
||||
sb.WriteString(`</nav>`)
|
||||
return sb.String(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user