change to new version

v3
veypi 6 months ago
parent b32b12878c
commit 5112f1ab7d

@ -1,40 +1,9 @@
major=$(strip $(shell awk -F ':' '/major/ {print $$2;}' cfg.yml))
minor=$(strip $(shell awk -F ':' '/minor/ {print $$2;}' cfg.yml))
patch=$(strip $(shell awk -F ':' '/patch/ {print $$2;}' cfg.yml))
version=v$(major).$(minor).$(patch)
vinfo=$(strip $(shell awk -F '$(version)' '/$(version)/ {print $$2;}' vuf.md))
version:
@echo $(version) $(vinfo)
addtag:
ifeq ($(vinfo),)
@echo please add version info in vuf.md
else
@git tag -m '$(vinfo)' $(version)
@git push origin $(version)
endif
dropTag:
@git tag -d $(version)
@git push origin :refs/tags/$(version)
#
# Makefile
# Copyright (C) 2024 veypi <i@veypi.com>
# 2025-03-04 16:08:06
# Distributed under terms of the GPL license.
#
run:
@cd oab && cargo run -- -c ./cfg-demo.yml
syncDB:
@scp -P 19529 oa.db root@alco.host:/root/
buildweb:
@cd oaweb && yarn build
@rm -rf ./oab/dist/*
@mv ./oaweb/dist/spa/* ./oab/dist/
nats:
@nats-server -c ./script/nats.cfg
.PHONY:oab
oab:
@cd oab && cargo run -- -c ./cfg-demo.yml
@go run ./cli/*.go -f ./cfg/dev.yml -l debug

@ -1,3 +0,0 @@
major: 1
minor: 0
patch: 0

@ -1,9 +0,0 @@
#
# Makefile
# Copyright (C) 2024 veypi <i@veypi.com>
# 2025-03-04 16:08:06
# Distributed under terms of the GPL license.
#
run:
@go run ./cli/*.go -f ./cfg/dev.yml -l debug

3
oa/.gitignore vendored

@ -1,3 +0,0 @@
build
static
dist

@ -1,16 +0,0 @@
#
# Makefile
# Copyright (C) 2024 veypi <i@veypi.com>
# 2024-10-22 16:36
# Distributed under terms of the GPL license.
#
cfg=~/.config/oa.yaml
run:
@go run main.go -f ${cfg}
dbrest:
@go run main.go -f ${cfg} db drop
@go run main.go -f ${cfg} db migrate
@go run main.go -f ${cfg} db init

@ -1,109 +0,0 @@
package access
import (
"github.com/veypi/OneBD/rest"
"oa/cfg"
M "oa/models"
)
func useAccess(r rest.Router) {
r.Get("/", accessList)
r.Post("/", accessPost)
r.Get("/:access_id", accessGet)
r.Patch("/:access_id", accessPatch)
r.Delete("/:access_id", accessDelete)
}
func accessList(x *rest.X) (any, error) {
opts := &M.AccessList{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := make([]*M.Access, 0, 10)
query := cfg.DB()
if opts.CreatedAt != nil {
query = query.Where("created_at > ?", opts.CreatedAt)
}
if opts.UpdatedAt != nil {
query = query.Where("updated_at > ?", opts.UpdatedAt)
}
query = query.Where("app_id LIKE ?", opts.AppID)
if opts.UserID != nil {
query = query.Where("user_id LIKE ?", opts.UserID)
}
if opts.RoleID != nil {
query = query.Where("role_id LIKE ?", opts.RoleID)
}
if opts.Name != nil {
query = query.Where("name LIKE ?", opts.Name)
}
err = query.Find(&data).Error
return data, err
}
func accessPost(x *rest.X) (any, error) {
opts := &M.AccessPost{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Access{}
data.AppID = opts.AppID
data.UserID = opts.UserID
data.RoleID = opts.RoleID
data.Name = opts.Name
data.TID = opts.TID
data.Level = opts.Level
err = cfg.DB().Create(data).Error
return data, err
}
func accessGet(x *rest.X) (any, error) {
opts := &M.AccessGet{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Access{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
return data, err
}
func accessDelete(x *rest.X) (any, error) {
opts := &M.AccessDelete{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Access{}
err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error
return data, err
}
func accessPatch(x *rest.X) (any, error) {
opts := &M.AccessPatch{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Access{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
if err != nil {
return nil, err
}
optsMap := make(map[string]interface{})
if opts.TID != nil {
optsMap["tid"] = opts.TID
}
if opts.Level != nil {
optsMap["level"] = opts.Level
}
err = cfg.DB().Model(data).Updates(optsMap).Error
return data, err
}

@ -1,16 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-09-23 16:28:13
// Distributed under terms of the MIT license.
//
// Auto generated by OneBD. DO NOT EDIT
package access
import (
"github.com/veypi/OneBD/rest"
)
func Use(r rest.Router) {
useAccess(r)
}

@ -1,157 +0,0 @@
package app
import (
"oa/cfg"
"oa/errs"
"oa/libs/auth"
M "oa/models"
"github.com/veypi/OneBD/rest"
"github.com/veypi/utils"
"gorm.io/gorm"
)
func useApp(r rest.Router) {
r.Delete("/:app_id", auth.Check("app", "app_id", auth.DoDelete), appDelete)
r.Get("/:app_id", auth.Check("app", "app_id", auth.DoRead), appGet)
r.Get("/:app_id/key", auth.Check("app", "app_id", auth.DoDelete), appKey)
r.Get("/", appList)
r.Patch("/:app_id", auth.Check("app", "app_id", auth.DoUpdate), appPatch)
r.Post("/", auth.Check("app", "", auth.DoCreate), appPost)
}
func appKey(x *rest.X) (any, error) {
id := x.Params.GetStr("app_id")
if id == "" {
return nil, errs.ArgsInvalid.WithStr("missing app_id")
}
data := &M.App{}
data.ID = id
key := utils.RandSeq(32)
// err := cfg.DB().Where("id = ?", x.Params.GetStr("app_id")).First(data).Error
err := cfg.DB().Model(data).Update("key", key).Error
return key, err
}
func appDelete(x *rest.X) (any, error) {
opts := &M.AppDelete{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.App{}
err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error
return data, err
}
func appGet(x *rest.X) (any, error) {
opts := &M.AppGet{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.App{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
return data, err
}
func appList(x *rest.X) (any, error) {
opts := &M.AppList{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := make([]*struct {
M.App
UserStatus string `json:"user_status"`
}, 0, 10)
token, err := auth.CheckJWT(x)
if err == nil {
uid := token.UID
query := cfg.DB().Table("apps").Select("apps.*,app_users.status user_status")
if opts.Name != nil {
query = query.Joins("LEFT JOIN app_users ON app_users.app_id = apps.id AND app_users.user_id = ? AND apps.name LIKE ?", uid, opts.Name)
} else {
query = query.Joins("LEFT JOIN app_users ON app_users.app_id = apps.id AND app_users.user_id = ?", uid)
}
err = query.Scan(&data).Error
} else {
err = cfg.DB().Table("apps").Select("id", "name", "icon").Find(&data).Error
}
// logv.AssertError(cfg.DB().Table("accesses a").
// Select("a.name, a.t_id, a.level").
// Joins("INNER JOIN user_roles ur ON ur.role_id = a.role_id AND ur.user_id = ?", refresh.UID).
// Scan(&acList).Error)
return data, err
}
func appPost(x *rest.X) (any, error) {
opts := &M.AppPost{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.App{}
data.Name = opts.Name
data.Icon = opts.Icon
data.Typ = opts.Typ
if opts.Des != nil {
data.Des = *opts.Des
}
err = cfg.DB().Transaction(func(tx *gorm.DB) error {
data.Key = utils.RandSeq(32)
err := tx.Create(data).Error
if err != nil {
return err
}
au := &M.AppUser{
AppID: data.ID,
UserID: x.Request.Context().Value("uid").(string),
Status: M.AUSTATUS_OK,
}
return tx.Create(au).Error
})
return data, err
}
func appPatch(x *rest.X) (any, error) {
opts := &M.AppPatch{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.App{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
if err != nil {
return nil, err
}
optsMap := make(map[string]interface{})
if opts.Name != nil {
optsMap["name"] = opts.Name
}
if opts.Icon != nil {
optsMap["icon"] = opts.Icon
}
if opts.Des != nil {
optsMap["des"] = opts.Des
}
if opts.Typ != nil {
optsMap["typ"] = opts.Typ
}
if opts.InitRoleID != nil {
optsMap["init_role_id"] = opts.InitRoleID
}
if opts.Status != nil {
optsMap["status"] = opts.Status
}
if opts.InitUrl != nil {
optsMap["init_url"] = opts.InitUrl
}
err = cfg.DB().Model(data).Updates(optsMap).Error
return data, err
}

@ -1,99 +0,0 @@
package app
import (
"github.com/google/uuid"
"github.com/veypi/OneBD/rest"
"oa/cfg"
M "oa/models"
"strings"
)
func useAppUser(r rest.Router) {
r.Post("/", appUserPost)
r.Delete("/:app_user_id", appUserDelete)
r.Get("/:app_user_id", appUserGet)
r.Get("/", appUserList)
r.Patch("/:app_user_id", appUserPatch)
}
func appUserPost(x *rest.X) (any, error) {
opts := &M.AppUserPost{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.AppUser{}
data.ID = strings.ReplaceAll(uuid.New().String(), "-", "")
data.AppID = opts.AppID
data.UserID = opts.UserID
data.Status = opts.Status
err = cfg.DB().Create(data).Error
return data, err
}
func appUserDelete(x *rest.X) (any, error) {
opts := &M.AppUserDelete{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.AppUser{}
err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error
return data, err
}
func appUserGet(x *rest.X) (any, error) {
opts := &M.AppUserGet{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.AppUser{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
return data, err
}
func appUserList(x *rest.X) (any, error) {
opts := &M.AppUserList{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := make([]*M.AppUser, 0, 10)
query := cfg.DB()
if opts.AppID != nil {
query = query.Where("app_id LIKE ?", opts.AppID)
}
if opts.UserID != nil {
query = query.Where("user_id LIKE ?", opts.UserID)
}
if opts.Status != nil {
query = query.Where("status LIKE ?", opts.Status)
}
err = query.Find(&data).Error
return data, err
}
func appUserPatch(x *rest.X) (any, error) {
opts := &M.AppUserPatch{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.AppUser{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
if err != nil {
return nil, err
}
optsMap := make(map[string]interface{})
if opts.Status != nil {
optsMap["status"] = opts.Status
}
err = cfg.DB().Model(data).Updates(optsMap).Error
return data, err
}

@ -1,19 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-09-23 16:28:13
// Distributed under terms of the MIT license.
//
// Auto generated by OneBD. DO NOT EDIT
package app
import (
"github.com/veypi/OneBD/rest"
)
func Use(r rest.Router) {
useApp(r)
useAppUser(r.SubRouter(":app_id/app_user"))
useResource(r.SubRouter(":app_id/resource"))
useRole(r.SubRouter(":app_id/role"))
}

@ -1,91 +0,0 @@
package app
import (
"github.com/veypi/OneBD/rest"
"oa/cfg"
M "oa/models"
)
func useResource(r rest.Router) {
r.Post("/", resourcePost)
r.Delete("/", resourceDelete)
r.Get("/", resourceList)
r.Delete("/:resource_id", resourceDelete)
r.Get("/:resource_id", resourceGet)
r.Patch("/:resource_id", resourcePatch)
}
func resourceGet(x *rest.X) (any, error) {
opts := &M.ResourceGet{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Resource{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
return data, err
}
func resourcePatch(x *rest.X) (any, error) {
opts := &M.ResourcePatch{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Resource{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
if err != nil {
return nil, err
}
optsMap := make(map[string]interface{})
if opts.Des != nil {
optsMap["des"] = opts.Des
}
err = cfg.DB().Model(data).Updates(optsMap).Error
return data, err
}
func resourceDelete(x *rest.X) (any, error) {
opts := &M.ResourceDelete{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Resource{}
err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error
return data, err
}
func resourceList(x *rest.X) (any, error) {
opts := &M.ResourceList{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := make([]*M.Resource, 0, 10)
query := cfg.DB()
if opts.AppID != nil {
query = query.Where("app_id LIKE ?", opts.AppID)
}
err = query.Find(&data).Error
return data, err
}
func resourcePost(x *rest.X) (any, error) {
opts := &M.ResourcePost{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Resource{}
data.AppID = opts.AppID
data.Name = opts.Name
data.Des = opts.Des
err = cfg.DB().Create(data).Error
return data, err
}

@ -1,96 +0,0 @@
package app
import (
"github.com/veypi/OneBD/rest"
"oa/cfg"
M "oa/models"
)
func useRole(r rest.Router) {
r.Delete("/:role_id", roleDelete)
r.Get("/", roleList)
r.Post("/", rolePost)
r.Get("/:role_id", roleGet)
r.Patch("/:role_id", rolePatch)
}
func roleDelete(x *rest.X) (any, error) {
opts := &M.RoleDelete{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Role{}
err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error
return data, err
}
func roleList(x *rest.X) (any, error) {
opts := &M.RoleList{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := make([]*M.Role, 0, 10)
query := cfg.DB()
if opts.AppID != nil {
query = query.Where("app_id LIKE ?", opts.AppID)
}
if opts.Name != nil {
query = query.Where("name LIKE ?", opts.Name)
}
err = query.Find(&data).Error
return data, err
}
func rolePost(x *rest.X) (any, error) {
opts := &M.RolePost{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Role{}
data.AppID = opts.AppID
data.Name = opts.Name
data.Des = opts.Des
err = cfg.DB().Create(data).Error
return data, err
}
func roleGet(x *rest.X) (any, error) {
opts := &M.RoleGet{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Role{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
return data, err
}
func rolePatch(x *rest.X) (any, error) {
opts := &M.RolePatch{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Role{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
if err != nil {
return nil, err
}
optsMap := make(map[string]interface{})
if opts.Name != nil {
optsMap["name"] = opts.Name
}
if opts.Des != nil {
optsMap["des"] = opts.Des
}
err = cfg.DB().Model(data).Updates(optsMap).Error
return data, err
}

@ -1,33 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-09-20 16:10:16
// Distributed under terms of the MIT license.
//
package api
import (
"oa/cfg"
"oa/libs"
"github.com/veypi/OneBD/rest"
"github.com/veypi/OneBD/rest/middlewares/crud"
)
func Use(r rest.Router) {
r.Set("/*", "OPTIONS", libs.CorsAllowAny)
r.Use(libs.CorsAllowAny)
r.Get("", baseInfo)
cfg.StaticObjs.RegistRouter(r, crud.ArgParser)
// access.Use(r.SubRouter("access"))
// app.Use(r.SubRouter("app"))
// user.Use(r.SubRouter("user"))
// token.Use(r.SubRouter("token"))
cfg.StaticObjs.RegistRouter(r, crud.CRUD)
}
func baseInfo(x *rest.X) (any, error) {
return map[string]any{
"id": cfg.Config.ID,
}, nil
}

@ -1,16 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-09-24 22:37:12
// Distributed under terms of the MIT license.
//
// Auto generated by OneBD. DO NOT EDIT
package token
import (
"github.com/veypi/OneBD/rest"
)
func Use(r rest.Router) {
useToken(r)
}

@ -1,258 +0,0 @@
package token
import (
"encoding/hex"
"encoding/json"
"net/http"
"oa/cfg"
"oa/errs"
"oa/libs/auth"
M "oa/models"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/veypi/OneBD/rest"
"github.com/veypi/utils"
"github.com/veypi/utils/logv"
)
func useToken(r rest.Router) {
r.Post("/salt", tokenSalt)
r.Post("/", tokenPost)
r.Get("/:token_id", tokenGet)
r.Patch("/:token_id", tokenPatch)
r.Delete("/:token_id", tokenDelete)
r.Get("/", tokenList)
}
func tokenSalt(x *rest.X) (any, error) {
opts := &M.TokenSalt{}
err := x.Parse(opts)
logv.Warn().Msg(opts.Username)
if err != nil {
return nil, err
}
data := &M.User{}
query := "username = ?"
if opts.Typ == nil {
} else if *opts.Typ == "email" {
query = "email = ?"
} else if *opts.Typ == "phone" {
query = "phone = ?"
}
err = cfg.DB().Where(query, opts.Username).First(data).Error
return map[string]string{"salt": data.Salt, "id": data.ID}, err
}
// for user login app
func tokenPost(x *rest.X) (any, error) {
opts := &M.TokenPost{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
aid := cfg.Config.ID
if opts.AppID != nil && *opts.AppID != "" {
aid = *opts.AppID
}
data := &M.Token{}
claim := &auth.Claims{}
claim.IssuedAt = jwt.NewNumericDate(time.Now())
claim.Issuer = cfg.Config.ID
if opts.Refresh != nil {
typ := "app"
if opts.Typ != nil {
typ = *opts.Typ
}
// for other app redirect
refresh, err := auth.ParseJwt(*opts.Refresh)
if err != nil {
return nil, err
}
if refresh.ID == "" {
return nil, errs.AuthInvalid
}
err = cfg.DB().Where("id = ?", refresh.ID).First(data).Error
if err != nil {
return nil, err
}
claim.AID = aid
claim.UID = refresh.UID
claim.Name = refresh.Name
claim.Icon = refresh.Icon
claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10))
if typ == "app" {
if refresh.AID == aid {
// refresh token
acList := make(auth.Access, 0, 10)
logv.AssertError(cfg.DB().Table("accesses a").
Select("a.name, a.t_id, a.level").
Joins("INNER JOIN user_roles ur ON ur.role_id = a.role_id AND ur.user_id = ? AND a.app_id = ?", refresh.UID, aid).
Scan(&acList).Error)
claim.Access = acList
if aid == cfg.Config.ID {
return auth.GenJwt(claim)
}
app := &M.App{}
err = cfg.DB().Where("id = ?", aid).First(app).Error
if err != nil {
return nil, err
}
return auth.GenJwtWithKey(claim, app.Key)
} else if aid != cfg.Config.ID {
// 只能生成其他应用的refresh token
newToken := &M.Token{}
newToken.UserID = refresh.UID
newToken.AppID = aid
newToken.ExpiredAt = time.Now().Add(time.Hour * 24)
if opts.OverPerm != nil {
newToken.OverPerm = *opts.OverPerm
}
if opts.Device != nil {
newToken.Device = *opts.Device
}
newToken.Ip = x.GetRemoteIp()
logv.AssertError(cfg.DB().Create(newToken).Error)
// gen other app token
claim.ID = newToken.ID
claim.ExpiresAt = jwt.NewNumericDate(newToken.ExpiredAt)
return auth.GenJwt(claim)
} else {
// 其他应用获取访问oa的token
claim.Access = make(auth.Access, 0, 10)
if data.OverPerm != "" {
err = json.Unmarshal([]byte(data.OverPerm), &claim.Access)
if err != nil {
return nil, err
}
}
return auth.GenJwt(claim)
}
} else if typ == "ufs" {
claim.AID = refresh.AID
claim.UID = refresh.UID
claim.Name = refresh.Name
claim.Icon = refresh.Icon
claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10))
claim.Access = auth.Access{
{Name: "fs", TID: "/", Level: auth.Do},
}
token := logv.AssertFuncErr(auth.GenJwt(claim))
cookie := &http.Cookie{
Name: "fstoken", // Cookie 的名称
Value: token, // Cookie 的值
Path: "/fs/u/", // Cookie 的路径,通常是根路径
MaxAge: 600, // Cookie 的最大年龄,单位是秒
HttpOnly: true, // 是否仅限 HTTP(S) 访问
Secure: false, // 是否通过安全连接传输 Cookie
}
http.SetCookie(x, cookie)
return token, nil
} else {
return nil, errs.ArgsInvalid
}
} else if opts.Code != nil && aid == cfg.Config.ID && opts.Salt != nil && opts.UserID != nil {
// for oa login
user := &M.User{}
err = cfg.DB().Where("id = ?", opts.UserID).Find(user).Error
if err != nil {
return nil, err
}
logv.Info().Str("user", user.ID).Msg("login")
code := *opts.Code
salt := logv.AssertFuncErr(hex.DecodeString(*opts.Salt))
key := logv.AssertFuncErr(hex.DecodeString(user.Code))
de, err := utils.AesDecrypt([]byte(code), key, salt)
if err != nil || de != user.ID {
return nil, errs.AuthFailed
}
data.UserID = *opts.UserID
data.AppID = aid
data.ExpiredAt = time.Now().Add(time.Hour * 72)
if opts.OverPerm != nil {
data.OverPerm = *opts.OverPerm
}
if opts.Device != nil {
data.Device = *opts.Device
}
data.Ip = x.GetRemoteIp()
logv.AssertError(cfg.DB().Create(data).Error)
claim.ID = data.ID
claim.AID = aid
claim.UID = user.ID
claim.Name = user.Username
claim.Icon = user.Icon
claim.ExpiresAt = jwt.NewNumericDate(data.ExpiredAt)
if user.Nickname != "" {
claim.Name = user.Nickname
}
return auth.GenJwt(claim)
} else {
return nil, errs.ArgsInvalid
}
}
func tokenGet(x *rest.X) (any, error) {
opts := &M.TokenGet{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Token{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
return data, err
}
func tokenPatch(x *rest.X) (any, error) {
opts := &M.TokenPatch{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Token{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
if err != nil {
return nil, err
}
optsMap := make(map[string]interface{})
if opts.ExpiredAt != nil {
optsMap["expired_at"] = opts.ExpiredAt
}
if opts.OverPerm != nil {
optsMap["over_perm"] = opts.OverPerm
}
err = cfg.DB().Model(data).Updates(optsMap).Error
return data, err
}
func tokenDelete(x *rest.X) (any, error) {
opts := &M.TokenDelete{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.Token{}
err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error
return data, err
}
func tokenList(x *rest.X) (any, error) {
opts := &M.TokenList{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := make([]*M.Token, 0, 10)
query := cfg.DB()
query = query.Where("user_id = ?", opts.UserID)
query = query.Where("app_id = ?", opts.AppID)
err = query.Limit(opts.Limit).Find(&data).Error
return data, err
}

@ -1,17 +0,0 @@
//
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-09-23 16:28:13
// Distributed under terms of the MIT license.
//
// Auto generated by OneBD. DO NOT EDIT
package user
import (
"github.com/veypi/OneBD/rest"
)
func Use(r rest.Router) {
useUser(r)
useUserRole(r.SubRouter(":user_id/user_role"))
}

@ -1,171 +0,0 @@
package user
import (
"fmt"
"math/rand"
"oa/cfg"
"oa/errs"
"oa/libs/auth"
M "oa/models"
"strings"
"time"
"github.com/google/uuid"
"github.com/veypi/OneBD/rest"
"gorm.io/gorm"
)
func useUser(r rest.Router) {
r.Delete("/:user_id", auth.Check("user", "user_id", auth.DoDelete), userDelete)
r.Get("/:user_id", auth.Check("user", "user_id", auth.DoRead), userGet)
r.Get("/", auth.Check("user", "", auth.DoRead), userList)
r.Patch("/:user_id", auth.Check("user", "user_id", auth.DoUpdate), userPatch)
r.Post("/", userPost)
}
func userDelete(x *rest.X) (any, error) {
opts := &M.UserDelete{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.User{}
err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error
return data, err
}
func userGet(x *rest.X) (any, error) {
opts := &M.UserGet{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.User{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
return data, err
}
func userList(x *rest.X) (any, error) {
opts := &M.UserList{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := make([]*M.User, 0, 10)
query := cfg.DB()
if opts.Username != nil {
query = query.Where("username LIKE ?", opts.Username)
}
if opts.Nickname != nil {
query = query.Where("nickname LIKE ?", opts.Nickname)
}
if opts.Email != nil {
query = query.Where("email LIKE ?", opts.Email)
}
if opts.Phone != nil {
query = query.Where("phone LIKE ?", opts.Phone)
}
if opts.Status != nil {
query = query.Where("status = ?", opts.Status)
}
err = query.Find(&data).Error
return data, err
}
func userPatch(x *rest.X) (any, error) {
opts := &M.UserPatch{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.User{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
if err != nil {
return nil, err
}
optsMap := make(map[string]interface{})
if opts.Username != nil {
optsMap["username"] = opts.Username
}
if opts.Nickname != nil {
optsMap["nickname"] = opts.Nickname
}
if opts.Icon != nil {
optsMap["icon"] = opts.Icon
}
if opts.Email != nil {
optsMap["email"] = opts.Email
}
if opts.Phone != nil {
optsMap["phone"] = opts.Phone
}
if opts.Status != nil {
optsMap["status"] = opts.Status
}
err = cfg.DB().Model(data).Updates(optsMap).Error
return data, err
}
func userPost(x *rest.X) (any, error) {
opts := &M.UserPost{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.User{}
data.ID = strings.ReplaceAll(uuid.New().String(), "-", "")
data.Username = opts.Username
data.Salt = opts.Salt
data.Code = opts.Code
if data.Username == "" || len(data.Salt) != 32 || len(data.Code) != 64 {
return nil, errs.ArgsInvalid.WithStr("username/salt/code length")
}
if opts.Nickname != nil {
data.Nickname = *opts.Nickname
}
if opts.Icon != nil {
data.Icon = *opts.Icon
} else {
data.Icon = fmt.Sprintf("https://public.veypi.com/img/avatar/%04d.jpg", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(220))
}
if opts.Email != nil {
data.Email = *opts.Email
}
if opts.Phone != nil {
data.Phone = *opts.Phone
}
data.Status = 1
err = cfg.DB().Transaction(func(tx *gorm.DB) error {
err := tx.Create(data).Error
if err != nil {
return err
}
app := &M.App{}
err = tx.Where("id = ?", cfg.Config.ID).First(app).Error
if err != nil {
return err
}
status := "ok"
switch app.Typ {
case "private":
return errs.AuthNoPerm.WithStr("not enable register")
case "apply":
status = "applying"
case "public":
}
if app.Typ != "public" {
}
return tx.Create(&M.AppUser{
UserID: data.ID,
AppID: cfg.Config.ID,
Status: status,
}).Error
})
return data, nil
}

@ -1,109 +0,0 @@
package user
import (
"github.com/veypi/OneBD/rest"
"oa/cfg"
M "oa/models"
)
func useUserRole(r rest.Router) {
r.Delete("/:user_role_id", userRoleDelete)
r.Get("/:user_role_id", userRoleGet)
r.Get("/", userRoleList)
r.Patch("/:user_role_id", userRolePatch)
r.Post("/", userRolePost)
}
func userRoleList(x *rest.X) (any, error) {
opts := &M.UserRoleList{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := make([]*struct {
M.UserRole
Username string `json:"username"`
Nickname string `json:"nickname"`
Icon string `json:"icon"`
RoleName string `json:"role_name"`
}, 0, 10)
// data := make([]*M.UserRole, 0, 10)
query := cfg.DB().Debug().Table("user_roles").Select("user_roles.*,users.username,users.nickname,users.icon,roles.name as role_name").
Joins("JOIN users ON users.id = user_roles.user_id").
Joins("JOIN roles ON roles.id = user_roles.role_id")
if opts.UserID != nil && *opts.UserID != "-" {
query = query.Where("user_id LIKE ?", opts.UserID)
}
if opts.RoleID != nil {
query = query.Where("role_id LIKE ?", opts.RoleID)
}
if opts.AppID != nil {
query = query.Where("app_id LIKE ?", opts.AppID)
}
if opts.Status != nil {
query = query.Where("status LIKE ?", opts.Status)
}
err = query.Scan(&data).Error
return data, err
}
func userRolePost(x *rest.X) (any, error) {
opts := &M.UserRolePost{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.UserRole{}
data.UserID = opts.UserID
data.RoleID = opts.RoleID
data.AppID = opts.AppID
data.Status = opts.Status
err = cfg.DB().Create(data).Error
return data, err
}
func userRoleGet(x *rest.X) (any, error) {
opts := &M.UserRoleGet{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.UserRole{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
return data, err
}
func userRolePatch(x *rest.X) (any, error) {
opts := &M.UserRolePatch{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.UserRole{}
err = cfg.DB().Where("id = ?", opts.ID).First(data).Error
if err != nil {
return nil, err
}
optsMap := make(map[string]interface{})
if opts.Status != nil {
optsMap["status"] = opts.Status
}
err = cfg.DB().Model(data).Updates(optsMap).Error
return data, err
}
func userRoleDelete(x *rest.X) (any, error) {
opts := &M.UserRoleDelete{}
err := x.Parse(opts)
if err != nil {
return nil, err
}
data := &M.UserRole{}
err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error
return data, err
}

@ -1,78 +0,0 @@
//
// app.go
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-10-22 15:42
// Distributed under terms of the GPL license.
//
package fs
import (
"net/http"
"oa/cfg"
"oa/errs"
"oa/libs/auth"
"oa/libs/webdav"
"os"
"strings"
"github.com/veypi/utils"
"github.com/veypi/utils/logv"
)
func NewAppFs(prefix string) func(http.ResponseWriter, *http.Request) {
if strings.HasSuffix(prefix, "/") {
prefix = prefix[:len(prefix)-1]
}
tmp := utils.PathJoin(cfg.Config.FsPath, "app")
if !utils.FileExists(tmp) {
logv.AssertError(os.MkdirAll(tmp, 0744))
}
client := webdav.NewWebdav(tmp)
client.Prefix = prefix
client.RootIndex = 3
client.GenSubPathFunc = func(r *http.Request) (string, error) {
// /:aid/*p
aid, root := getid(r.URL.Path, prefix)
if root == "/" {
// if !utils.FileExists(tmp + "/" + aid) {
// os.MkdirAll(tmp+"/"+aid, 0744)
// }
}
if aid == "" {
return "", errs.AuthNoPerm
}
if root == "/pub" || strings.HasPrefix(root, "/pub/") {
switch r.Method {
case "GET", "HEAD", "POST":
return "", nil
default:
}
}
// appfs权限等于app权限
// TODO: 存在空文件覆盖重要文件的风险
handlerLevle := auth.Do
switch r.Method {
case "PUT", "MKCOL", "COPY", "MOVE":
handlerLevle = auth.DoCreate
case "DELETE":
handlerLevle = auth.DoDelete
case "OPTIONS":
// options请求不需要权限
return "", nil
default:
handlerLevle = auth.DoRead
}
payload, err := getToken(r)
if err != nil {
return "", err
}
if payload.Access.Check("app", aid, handlerLevle) {
return "", nil
}
return "", errs.AuthNoPerm
}
return client.ServeHTTP
}

@ -1,18 +0,0 @@
//
// fs.go
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-10-22 15:51
// Distributed under terms of the GPL license.
//
package fs
import (
"oa/libs/webdav"
)
func NewFs(dir_path, prefix string) *webdav.Handler {
client := webdav.NewWebdav(dir_path)
client.Prefix = prefix
return client
}

@ -1,120 +0,0 @@
//
// user.go
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-10-22 15:49
// Distributed under terms of the GPL license.
//
package fs
import (
"bufio"
"encoding/base64"
"net/http"
"oa/cfg"
"oa/errs"
"oa/libs/auth"
"oa/libs/webdav"
"os"
"strings"
"github.com/veypi/utils"
"github.com/veypi/utils/logv"
)
func NewUserFs(prefix string) func(http.ResponseWriter, *http.Request) {
if strings.HasSuffix(prefix, "/") {
prefix = prefix[:len(prefix)-1]
}
tmp := utils.PathJoin(cfg.Config.FsPath, "u")
if !utils.FileExists(tmp) {
logv.AssertError(os.MkdirAll(tmp, 0744))
}
client := webdav.NewWebdav(tmp)
client.Prefix = prefix
client.RootIndex = 4
client.Logger = func(r *http.Request, err error) {
}
client.GenSubPathFunc = func(r *http.Request) (string, error) {
// /:aid/*p
uid, root := getid(r.URL.Path, prefix)
if root == "/" {
if !utils.FileExists(tmp + "/" + uid) {
os.MkdirAll(tmp+"/"+uid, 0744)
}
}
if r.Method == "OPTIONS" {
return "", nil
}
payload, err := getToken(r)
if err != nil {
return "", err
}
if payload.Access.CheckPrefix("fs", root, auth.Do) && payload.UID == uid {
return "", nil
}
return "", errs.AuthNoPerm
}
return client.ServeHTTP
}
func getid(url, prefix string) (string, string) {
if strings.HasSuffix(prefix, "/") {
prefix = prefix[:len(prefix)-1]
}
dir := strings.TrimPrefix(url, prefix)
dirs := strings.Split(dir[1:], "/")
id := ""
root := "/"
if len(dirs) > 0 {
id = dirs[0]
}
if len(dirs) > 1 {
root = "/" + strings.Join(dirs[1:], "/")
}
return id, root
}
func getToken(r *http.Request) (*auth.Claims, error) {
authHeader := r.Header.Get("Authorization")
token := ""
if authHeader != "" {
typ := ""
if tags := strings.Split(authHeader, " "); len(tags) > 1 {
typ = strings.ToLower(tags[0])
}
if typ == "basic" {
decodedAuth, err := base64.StdEncoding.DecodeString(authHeader[6:])
if err != nil {
return nil, errs.AuthInvalid
}
// 通常认证信息格式为 username:password
credentials := string(decodedAuth)
reader := bufio.NewReader(strings.NewReader(credentials))
username, _ := reader.ReadString(':')
password := credentials[len(username):]
username = strings.TrimSuffix(username, "\n")
username = strings.TrimSuffix(username, ":")
token = strings.TrimPrefix(password, "\n")
} else if typ == "bearer" {
token = authHeader[7:]
}
} else {
acookie, err := r.Cookie("fstoken")
if err == nil {
token = acookie.Value
}
}
if token == "" {
return nil, errs.AuthNotFound
}
payload, err := auth.ParseJwt(token)
if err != nil {
return nil, err
}
return payload, nil
}

@ -1,41 +0,0 @@
//
// init.go
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-10-18 17:07
// Distributed under terms of the GPL license.
//
package builtin
import (
"net/http"
"net/http/httputil"
"net/url"
"oa/builtin/fs"
"oa/cfg"
"oa/libs"
"github.com/veypi/OneBD/rest"
"github.com/veypi/utils/logv"
)
func Enable(app *rest.Application) {
if cfg.Config.FsPath != "" {
r := app.Router().SubRouter("fs")
r.Set("/*", "OPTIONS", libs.CorsAllowAny)
r.Use(libs.CorsAllowAny)
r.Any("/a/:aid/*p", fs.NewAppFs("/fs/a"))
r.Any("/u/:uid/*p", fs.NewUserFs("/fs/u"))
}
tsPorxy := httputil.NewSingleHostReverseProxy(logv.AssertFuncErr(url.Parse("http://v.v:8428")))
fsProxy := fs.NewFs("/home/v/cache/", "")
app.SetMux(func(w http.ResponseWriter, r *http.Request) func(http.ResponseWriter, *http.Request) {
if r.Host == "ts.oa.v" || r.Header.Get("mux") == "ts" {
return tsPorxy.ServeHTTP
} else if r.Host == "fs.oa.v" {
return fsProxy.ServeHTTP
}
return nil
})
}

@ -1,54 +0,0 @@
//
// cfg.go
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-09-20 16:10:16
// Distributed under terms of the MIT license.
//
package cfg
import (
"github.com/veypi/OneBD/rest"
"github.com/veypi/utils"
"github.com/veypi/utils/flags"
"github.com/veypi/utils/logv"
)
type config struct {
rest.RestConf
DSN string `json:"dsn"`
ID string `json:"id"`
Key string `json:"key"`
FsPath string `json:"fs"`
AccessUrl string `json:"access_url"`
}
var Config = &config{}
var CMD = flags.New("oa", "the backend server of oa")
var CfgDump = CMD.SubCommand("cfg", "generate cfg file")
var configFile = CMD.String("f", "./dev.yaml", "the config file")
func init() {
CMD.StringVar(&Config.Host, "h", "0.0.0.0", "host")
CMD.IntVar(&Config.Port, "p", 4000, "port")
CMD.StringVar(&Config.LoggerLevel, "l", "info", "log level")
CMD.StringVar(&Config.DSN, "dsn", "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8&parseTime=True&loc=Local", "data source name")
CMD.Before = func() error {
flags.LoadCfg(*configFile, Config)
CMD.Parse()
if Config.Key == "" {
Config.Key = utils.RandSeq(32)
}
if Config.ID == "" {
Config.ID = utils.RandSeq(32)
}
logv.SetLevel(logv.AssertFuncErr(logv.ParseLevel(Config.LoggerLevel)))
return nil
}
CfgDump.Command = func() error {
flags.DumpCfg(*configFile, Config)
return nil
}
}

@ -1,47 +0,0 @@
//
// db.go
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-09-20 16:10:16
// Distributed under terms of the MIT license.
//
package cfg
import (
"github.com/veypi/OneBD/rest/middlewares/crud"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var db *gorm.DB
var CmdDB = CMD.SubCommand("db", "database operations")
var StaticObjs = crud.New()
func init() {
CmdDB.SubCommand("migrate", "migrate database").Command = func() error {
return crud.AutoMigrate(DB(), StaticObjs)
}
CmdDB.SubCommand("drop", "drop database").Command = func() error {
return crud.AutoDrop(DB(), StaticObjs)
}
}
func DB() *gorm.DB {
if db == nil {
var err error
db, err = gorm.Open(mysql.New(mysql.Config{
DSN: Config.DSN,
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
// db, err = gorm.Open(postgres.Open(Config.DSN), &gorm.Config{
// Logger: logger.Default.LogMode(logger.Silent),
// })
if err != nil {
panic(err)
}
}
return db
}

@ -1,26 +0,0 @@
//
// nats.go
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-10-11 17:32
// Distributed under terms of the MIT license.
//
package cfg
import (
"github.com/nats-io/nats-server/v2/server"
"github.com/veypi/utils/logv"
)
func RunNats() {
opts := &server.Options{
ConfigFile: "~/.config/oa/nats.cfg2",
}
ns, err := server.NewServer(opts)
if err != nil {
panic(err)
}
// Start the nats server
logv.Info().Msg("nats server start")
ns.Start()
}

@ -1,105 +0,0 @@
//
// errors.go
// Copyright (C) 2024 veypi <i@veypi.com>
// 2024-08-01 15:40
// Distributed under terms of the MIT license.
//
package errs
import (
"errors"
"fmt"
"net/http"
"github.com/go-sql-driver/mysql"
"github.com/veypi/OneBD/rest"
"github.com/veypi/utils/logv"
"gorm.io/gorm"
)
func JsonResponse(x *rest.X, data any) error {
x.WriteHeader(http.StatusOK)
return x.JSON(map[string]any{"code": 0, "data": data})
}
func JsonErrorResponse(x *rest.X, err error) {
code, msg := errIter(err)
x.WriteHeader(code / 100)
x.JSON(map[string]any{"code": code, "err": msg})
}
func errIter(err error) (code int, msg string) {
code = 50000
msg = err.Error()
switch e := err.(type) {
case *CodeErr:
code = e.Code
msg = e.Msg
case *mysql.MySQLError:
if e.Number == 1062 {
code = DuplicateKey.Code
msg = DuplicateKey.Msg
} else {
logv.Warn().Msgf("unhandled db error %d: %s", e.Number, err)
msg = "db error"
}
case interface{ Unwrap() error }:
code, _ = errIter(e.Unwrap())
default:
if errors.Is(e, gorm.ErrRecordNotFound) {
code = ResourceNotFound.Code
msg = ResourceNotFound.Msg
} else if errors.Is(e, rest.ErrParse) {
code = ArgsInvalid.Code
msg = e.Error()
} else {
logv.Warn().Msgf("unhandled error type: %T\n%s", err, err)
msg = e.Error()
}
}
return
}
type CodeErr struct {
Code int
Msg string
}
func (c *CodeErr) Error() string {
return fmt.Sprintf("code: %d, msg: %s", c.Code, c.Msg)
}
func (c *CodeErr) WithErr(e error) error {
nerr := &CodeErr{
Code: c.Code,
Msg: fmt.Errorf("%s: %w", c.Msg, e).Error(),
}
return nerr
}
func (c *CodeErr) WithStr(m string) error {
nerr := &CodeErr{
Code: c.Code,
Msg: fmt.Errorf("%s: %s", c.Msg, m).Error(),
}
return nerr
}
// New creates a new CodeMsg.
func New(code int, msg string) *CodeErr {
return &CodeErr{Code: code, Msg: msg}
}
var (
ArgsInvalid = New(40001, "args invalid")
DuplicateKey = New(40002, "duplicate key")
AuthNotFound = New(40100, "auth not found")
AuthFailed = New(40101, "auth failed")
AuthExpired = New(40102, "auth expired")
AuthInvalid = New(40103, "auth invalid")
AuthNoPerm = New(40104, "no permission")
NotFound = New(40400, "not found")
ResourceNotFound = New(40401, "resource not found")
DBError = New(50010, "db error")
)

@ -1,50 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Music Player</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="flex items-center justify-center min-h-screen bg-gray-900">
<div class="bg-gray-800 p-6 rounded-lg shadow-lg text-white w-full max-w-md space-y-4">
<audio id="player" src="https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"></audio>
<button id="playButton"
class="w-full py-2 px-4 bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition duration-300 ease-in-out transform active:scale-95 flex items-center justify-center space-x-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="h-6 w-6">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.26a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Play</span>
</button>
<progress id="progressBar" value="0" max="1" class="w-full h-2 bg-gray-700 rounded-full overflow-hidden">
<div class="h-full bg-green-500 rounded-full"></div>
</progress>
</div>
<script>
const player = document.getElementById('player');
const playButton = document.getElementById('playButton');
const progressBar = document.getElementById('progressBar');
playButton.addEventListener('click', () => {
if (player.paused) {
player.play();
playButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="h-6 w-6"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg><span>Pause</span>';
} else {
player.pause();
playButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="h-6 w-6"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.26a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/></svg><span>Play</span>';
}
});
player.addEventListener('timeupdate', () => {
console.log(player.currentTime, player.duration);
progressBar.value = player.currentTime / player.duration;
});
</script>
</body>
</html>

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Card</title>
<script src='v.js'></script>
</head>
<body>
<div class="h-6 w-40 bg-green-100 flex gap-4">
<slot name="a">slot a</slot>
</div>
<div class="h-14 w-40">
<slot>
<div ref="audio"></div>
</slot>
</div>
</body>
<script>
return {
data: {a: 2}
}
</script>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

@ -1,200 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>公司网站</title>
<script src="https://cdn.tailwindcss.com"></script>
<script type="module" src="./v.js"></script>
<script type="module" src="./t.js"></script>
<style>
div[ref] {
position: relative;
background: #999;
width: 10rem;
height: 10rem;
}
div[ref]::before {
content: 'dom组件';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #666;
font-size: 14px;
z-index: 0;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fade-in {
animation: fadeIn 1s ease-in-out;
}
</style>
</head>
<body class="bg-gray-100 text-gray-800">
<div id='app'>
<!-- 导航栏 -->
<nav class="fixed w-full bg-white shadow-lg z-50">
<div class="container mx-auto px-6 py-4 flex justify-between items-center">
<div class="flex items-center">
<a href="#" class="text-xl font-bold text-gray-800">公司名称</a>
</div>
<ul class="hidden md:flex space-x-6">
<li><a href="#about" class="hover:text-blue-500 transition duration-300">关于我们</a></li>
<li><a href="#services" class="hover:text-blue-500 transition duration-300">服务</a></li>
<li><a href="#cases" class="hover:text-blue-500 transition duration-300">案例研究</a></li>
<li><a href="#testimonials" class="hover:text-blue-500 transition duration-300">客户评价</a></li>
<li><a href="#contact" class="hover:text-blue-500 transition duration-300">联系我们</a></li>
</ul>
<button class="md:hidden focus:outline-none">
<svg class="h-6 w-6" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
viewBox="0 0 24 24" stroke="currentColor">
<path d="M4 6h16M4 12h16m-7 6h7"></path>
</svg>
</button>
</div>
</nav>
<!-- 首页 -->
<div id="home" class="relative h-screen flex items-center justify-center bg-cover bg-no-repeat fade-in"
style="background-image:url('https://picsum.photos/1600/900');">
<div class="absolute inset-0 bg-black opacity-50"></div>
<div class="relative text-center max-w-2xl mx-auto p-4">
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-extrabold tracking-tight text-white mb-6">欢迎来到我们的公司</h1>
<p class="text-lg sm:text-xl leading-relaxed text-white">我们专注于提供高质量的服务和解决方案。</p>
<a href="#contact"
class="mt-8 inline-block px-6 py-3 bg-blue-500 hover:bg-blue-600 rounded-md text-white font-semibold transition duration-300">联系我们</a>
<div ref="./t2/ta.js"></div>
<div class="text-red-400" ref="./t2/input.js"></div>
<div id="terminal"></div>
</div>
</div>
<!-- 关于我们 -->
<div id="about" class="py-20">
<div class="container mx-auto px-6">
<div class="max-w-4xl mx-auto text-center fade-in">
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight mb-6">关于我们</h2>
<p class="text-lg sm:text-xl leading-relaxed text-gray-600">我们是一家致力于提供优质服务和技术支持的公司。</p>
</div>
<div class="mt-12 grid grid-cols-1 md:grid-cols-2 gap-12">
<div ref="t2/companyHistory" class="fade-in"></div>
<div class='abmem' ref="t2/teamMembers" class="fade-in"></div>
</div>
</div>
</div>
<!-- 服务 -->
<div id="services" class="py-20 bg-gray-200">
<div class="container mx-auto px-6">
<div class="max-w-4xl mx-auto text-center fade-in">
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight mb-6">我们的服务</h2>
<p class="text-lg sm:text-xl leading-relaxed text-gray-600">我们提供一系列专业服务,满足不同客户的需求。</p>
</div>
<div class="mt-12 grid grid-cols-1 md:grid-cols-3 gap-12">
<div ref="serviceOne" class="fade-in"></div>
<div ref="serviceTwo" class="fade-in"></div>
<div ref="serviceThree" class="fade-in"></div>
</div>
</div>
</div>
<!-- 案例研究 -->
<div id="cases" class="py-20">
<div class="container mx-auto px-6">
<div class="max-w-4xl mx-auto text-center fade-in">
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight mb-6">案例研究</h2>
<p class="text-lg sm:text-xl leading-relaxed text-gray-600">查看我们过去成功的项目案例。</p>
</div>
<div class="mt-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12">
<div ref="caseStudyOne" class="fade-in"></div>
<div ref="caseStudyTwo" class="fade-in"></div>
<div ref="caseStudyThree" class="fade-in"></div>
</div>
</div>
</div>
<!-- 客户评价 -->
<div id="testimonials" class="py-20 bg-gray-200">
<div class="container mx-auto px-6">
<div class="max-w-4xl mx-auto text-center fade-in">
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight mb-6">客户评价</h2>
<p class="text-lg sm:text-xl leading-relaxed text-gray-600">听听我们的客户怎么说。</p>
</div>
<div class="mt-12 grid grid-cols-1 md:grid-cols-2 gap-12">
<div ref="testimonialOne" class="fade-in"></div>
<div ref="testimonialTwo" class="fade-in"></div>
</div>
</div>
</div>
<!-- 联系我们 -->
<div id="contact" class="py-20">
<div class="container mx-auto px-6">
<div class="max-w-4xl mx-auto text-center fade-in">
<h2 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight mb-6">联系我们</h2>
<p class="text-lg sm:text-xl leading-relaxed text-gray-600">如果您有任何问题或需要帮助,请随时与我们联系。</p>
</div>
<div class="mt-12 flex justify-center">
<div ref="contactForm" class="w-full max-w-md fade-in"></div>
</div>
</div>
</div>
<!-- 底部 -->
<footer class="bg-gray-800 text-white py-8">
<div class="container mx-auto px-6">
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="mb-4 md:mb-0">
<p>&copy; 2023 公司名称. All rights reserved.</p>
</div>
<div>
<ul class="flex space-x-6">
<li><a href="#" class="hover:text-gray-300 transition duration-300">隐私政策</a></li>
<li><a href="#" class="hover:text-gray-300 transition duration-300">条款条件</a></li>
</ul>
</div>
</div>
</div>
</footer>
<script type="module">
import v from './vyes.js'
// 添加滚动监听事件以激活导航栏高亮
document.addEventListener("scroll", function () {
const sections = ["home", "about", "services", "cases", "testimonials", "contact"];
let currentSection = "";
sections.forEach(section => {
const element = document.getElementById(section);
if (element.offsetTop <= window.scrollY + 100) {
currentSection = section;
}
});
sections.forEach(section => {
const link = document.querySelector(`a[href="#${section}"]`);
if (link) {
link.classList.toggle("text-blue-500", section === currentSection);
}
});
});
</script>
</div>
</body>
</html>

@ -1,12 +0,0 @@
<head>
<meta charset="utf-8" />
<title>Default</title>
</head>
<div>
<div class="header flex"></div>
<slot></slot>
</div>
<script>
console.log('default')
</script>

@ -1,6 +0,0 @@
<body>
<slot>></slot>
</body>
<script>
</script>

@ -1,183 +0,0 @@
(function () {
const style = document.createElement('style');
// 定义 CSS 文本
const cssText = `
.selected {
outline: 2px solid #44f;
opacity: 1;
pointer-events: auto;
}
.panel {
position: fixed;
left: 0;
top: 0;
z-index: 9999;
width: 300px;
height: 100%;
background-color: #f4f4f4;
border-left: 1px solid #ccc;
padding: 10px;
box-shadow: -2px 0 5px rgba(0,0,0,0.1);
display: none;
resize: both;
overflow: auto;
}
.resizer {
position: absolute;
right: 0;
bottom: 0;
width: 10px;
height: 10px;
cursor: se-resize;
background-color: #aaa;
}
.domline {
width: 100%;
}
.domline:hover {
opacity: 0.7;
}
`;
style.innerHTML = cssText;
let selectedElement = null;
const panel = document.createElement('div');
panel.classList.add('panel');
const treeContainer = document.createElement('div');
treeContainer.style.marginBottom = '10px';
const htmlEditor = document.createElement('textarea');
htmlEditor.style.width = '100%';
htmlEditor.rows = '10';
htmlEditor.cols = '30';
htmlEditor.placeholder = 'Edit element innerHTML here...';
const header = document.createElement('div');
header.style.cursor = 'move';
header.style.backgroundColor = '#ddd';
header.style.padding = '5px';
header.textContent = 'DOM Inspector';
panel.appendChild(style);
panel.appendChild(header);
panel.appendChild(treeContainer);
panel.appendChild(htmlEditor);
document.body.appendChild(panel);
function createTree(node, parent, iter = 0) {
if (!node || node.nodeType !== Node.ELEMENT_NODE) return;
if (iter > 3) {
return;
}
const item = document.createElement('div');
item.style.cursor = 'pointer';
item.style.marginLeft = '10px';
const line = document.createElement('div');
line.textContent = node.tagName.toLowerCase();
line.classList.add('domline');
line.addEventListener('click', (e) => {
console.log(node)
e.preventDefault();
selectElement(node);
});
item.appendChild(line);
parent.appendChild(item);
Array.from(node.children).forEach(child => {
createTree(child, item, iter + 1);
});
}
function selectElement(element) {
if (selectedElement) {
selectedElement.classList.remove('selected')
// document.querySelectorAll('.blurred').forEach(el => el.classList.remove('blurred'));
}
selectedElement = element;
selectedElement.classList.add('selected')
updatePanel(selectedElement);
// let pele = element.parentElement
// while (pele && pele !== document.documentElement) {
// pele.classList.add('blurred');
// pele = pele.parentElement;
// }
}
function updatePanel(element) {
treeContainer.innerHTML = '';
if (element.parentElement) {
const parentItem = document.createElement('div');
parentItem.textContent = `parent (${element.parentElement.tagName.toLowerCase()})`;
parentItem.style.cursor = 'pointer';
parentItem.style.color = 'blue';
parentItem.style.fontWeight = 'bold';
parentItem.addEventListener('click', () => {
selectElement(element.parentElement);
});
treeContainer.appendChild(parentItem);
}
createTree(element, treeContainer);
htmlEditor.value = element.outerHTML || '';
htmlEditor.oninput = () => {
element.outerHTML = htmlEditor.value;
};
}
document.addEventListener('keydown', event => {
if (event.key === 'Control') {
document.addEventListener('mousedown', mouseDownHandler);
}
});
document.addEventListener('keyup', event => {
if (event.key === 'Control') {
document.removeEventListener('mousedown', mouseDownHandler);
}
});
function mouseDownHandler(event) {
if (event.button === 0 && event.ctrlKey) {
event.preventDefault();
selectElement(event.target);
panel.style.display = 'block';
}
}
// Add draggable functionality to the sidebar
let isDragging = false;
let dragOffsetX, dragOffsetY;
header.addEventListener('mousedown', (e) => {
isDragging = true;
dragOffsetX = e.clientX - panel.offsetLeft;
dragOffsetY = e.clientY - panel.offsetTop;
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
let top = e.clientY - dragOffsetY;
if (top < 0) top = 0;
let left = e.clientX - dragOffsetX;
if (left < 0) left = 0;
panel.style.left = `${left}px`;
panel.style.top = `${top}px`;
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
})();

@ -1,56 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Company History</title>
</head>
<body class="p-6 bg-white rounded-lg shadow-md">
<div class="max-w-4xl mx-auto">
<h3 class="text-2xl sm:text-3xl font-bold tracking-tight mb-6">公司历史</h3>
<div
class="timeline relative before:absolute before:left-5 before:top-0 before:w-0.5 before:h-full before:bg-gray-300">
<!-- Timeline Item 1 -->
<div class="mb-8 ml-8 last:mb-0">
<div class="absolute w-3 h-3 bg-blue-500 rounded-full mt-1.5 -left-1.5 border border-white"></div>
<time class="mb-1 text-sm font-normal leading-none text-gray-400">2010年</time>
<h3 class="text-lg font-semibold text-gray-900">成立</h3>
<p class="text-base font-normal text-gray-500">公司在硅谷创立,旨在提供创新的技术解决方案。</p>
</div>
<!-- Timeline Item 2 -->
<div class="mb-8 ml-8 last:mb-0">
<div class="absolute w-3 h-3 bg-blue-500 rounded-full mt-1.5 -left-1.5 border border-white"></div>
<time class="mb-1 text-sm font-normal leading-none text-gray-400">2012年</time>
<h3 class="text-lg font-semibold text-gray-900">第一次产品发布</h3>
<p class="text-base font-normal text-gray-500">发布了第一款软件产品,获得了市场的好评。</p>
</div>
<!-- Timeline Item 3 -->
<div class="mb-8 ml-8 last:mb-0">
<div class="absolute w-3 h-3 bg-blue-500 rounded-full mt-1.5 -left-1.5 border border-white"></div>
<time class="mb-1 text-sm font-normal leading-none text-gray-400">2015年</time>
<h3 class="text-lg font-semibold text-gray-900">全球扩张</h3>
<p class="text-base font-normal text-gray-500">开始在全球范围内扩展业务,设立了欧洲办事处。</p>
</div>
<!-- Timeline Item 4 -->
<div class="mb-8 ml-8 last:mb-0">
<div class="absolute w-3 h-3 bg-blue-500 rounded-full mt-1.5 -left-1.5 border border-white"></div>
<time class="mb-1 text-sm font-normal leading-none text-gray-400">2020年</time>
<h3 class="text-lg font-semibold text-gray-900">技术革新</h3>
<p class="text-base font-normal text-gray-500">引入最新的AI技术提升了产品的智能化水平。</p>
</div>
<!-- Timeline Item 5 -->
<div class="mb-8 ml-8 last:mb-0">
<div class="absolute w-3 h-3 bg-blue-500 rounded-full mt-1.5 -left-1.5 border border-white"></div>
<time class="mb-1 text-sm font-normal leading-none text-gray-400">2023年</time>
<h3 class="text-lg font-semibold text-gray-900">未来展望</h3>
<p class="text-base font-normal text-gray-500">持续研发新技术,致力于成为行业的领导者。</p>
</div>
</div>
</div>
</body>
</html>

@ -1,134 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Data Structure Topology with D3.js</title>
<style>
.node {
fill: #f9f9f9;
stroke: #ccc;
stroke-width: 2px;
}
.field {
font-size: 14px;
font-family: Arial, sans-serif;
}
.edge {
fill: none;
stroke: #333;
stroke-width: 2px;
}
.label {
font-size: 12px;
font-family: Arial, sans-serif;
fill: #555;
}
</style>
</head>
<body>
<div id="graph"></div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
const data = {
nodes: [
{id: "Node A", fields: ["Field1: int", "Field2: string"]},
{id: "Node B", fields: ["Field1: float", "Field2: bool"]},
{id: "Node C", fields: ["Field1: date", "Field2: char"]}
],
links: [
{source: "Node A", target: "Node B", type: "One-to-One"},
{source: "Node A", target: "Node C", type: "One-to-Many"},
{source: "Node B", target: "Node C", type: "Many-to-Many"}
]
};
const width = 800;
const height = 600;
const svg = d3.select("#graph")
.append("svg")
.attr("width", width)
.attr("height", height);
const simulation = d3.forceSimulation(data.nodes)
.force("link", d3.forceLink(data.links).id(d => d.id))
.force("charge", d3.forceManyBody().strength(-400))
.force("center", d3.forceCenter(width / 2, height / 2));
const linkElements = svg.selectAll(".edge")
.data(data.links)
.enter()
.append("line")
.attr("class", "edge");
const nodeElements = svg.selectAll(".node")
.data(data.nodes)
.enter()
.append("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
nodeElements.append("rect")
.attr("x", -75)
.attr("y", -50)
.attr("width", 150)
.attr("height", 100)
.attr("rx", 10)
.attr("ry", 10);
nodeElements.append("text")
.attr("dx", 0)
.attr("dy", -20)
.text(d => d.id)
.attr("text-anchor", "middle")
.attr("class", "field");
nodeElements.selectAll(".field-text")
.data(d => d.fields)
.enter()
.append("text")
.attr("dx", 0)
.attr("dy", (d, i) => i * 20 + 5)
.text(d => d)
.attr("text-anchor", "middle")
.attr("class", "field");
simulation.on("tick", () => {
linkElements
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
nodeElements.attr("transform", d => `translate(${d.x},${d.y})`);
});
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
</script>
</body>
</html>

@ -1,25 +0,0 @@
import v, { proxy } from '../vyes.js'
const data = proxy.Watch({ a: 2 })
const body = v('bg-gray-200 flex gap-4 h-12 items-center justify-evenly rounded', [
v('', [v('span', 'count:'), v('span', () => data.a)]),
v({
typ: 'input',
vbind: [data, 'a']
}),
v({
class: 'select-none cursor-pointer hover:bg-gray-300 p-2 rounded',
children: 'add',
onclick: () => {
data.a++
}
}),
])
export const setup = (node, data) => {
console.log(node, data)
}
export default body

@ -1,16 +0,0 @@
/*
* ta.js
* Copyright (C) 2024 veypi <i@veypi.com>
*
* Distributed under terms of the GPL license.
*/
import v from '../vyes.js'
const body = v('', '123')
export const setup = (node, data) => {
console.log(node, data)
}
export default body

@ -1,128 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Team Members</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<style>
@keyframes fade-In {
from {
opacity: 1;
transform: translateY(80px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
div[datavv] .fade-in {
animation: fade-In 2s ease-in-out;
}
</style>
</head>
<body class="p-6 bg-white rounded-lg shadow-md">
<div class="max-w-4xl mx-auto" datavv>
<h3 class="text-2xl sm:text-3xl font-bold tracking-tight mb-6">我们的团队</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<!-- Team Member 1 -->
<div class="bg-white p-6 rounded-lg shadow-md fade-in">
<img src="https://picsum.photos/150/150/" alt="John Doe" class="w-full h-48 object-cover rounded-t-lg">
<div class="mt-4">
<h4 class="text-xl font-semibold text-gray-900">John Doe</h4>
<p class="text-gray-500">首席执行官</p>
<p class="mt-2 text-base font-normal text-gray-700">拥有超过15年的行业经验擅长战略规划和领导。</p>
</div>
</div>
<!-- Team Member 2 -->
<div class="bg-white p-6 rounded-lg shadow-md fade-in">
<img src="https://picsum.photos/150/150/" alt="Jane Smith" class="w-full h-48 object-cover rounded-t-lg">
<div class="mt-4">
<h4 class="text-xl font-semibold text-gray-900">Jane Smith</h4>
<p class="text-gray-500">首席技术官</p>
<p class="mt-2 text-base font-normal text-gray-700">专注于技术创新,带领团队开发前沿产品。</p>
</div>
</div>
<!-- Team Member 3 -->
<div class="bg-white p-6 rounded-lg shadow-md fade-in">
<img src="https://picsum.photos/150/150/" alt="Sam Johnson" class="w-full h-48 object-cover rounded-t-lg">
<div class="mt-4">
<h4 class="text-xl font-semibold text-gray-900">Sam Johnson</h4>
<p class="text-gray-500">市场总监</p>
<p class="mt-2 text-base font-normal text-gray-700">精通市场营销策略,推动品牌知名度提升。</p>
</div>
</div>
<!-- Team Member 4 -->
<div class="bg-white p-6 rounded-lg shadow-md fade-in">
<img src="https://picsum.photos/150/150/" alt="Emily Davis" class="w-full h-48 object-cover rounded-t-lg">
<div class="mt-4">
<h4 class="text-xl font-semibold text-gray-900">Emily Davis</h4>
<p class="text-gray-500">人力资源经理</p>
<p class="mt-2 text-base font-normal text-gray-700">负责招聘和员工发展,营造积极的工作环境。</p>
</div>
</div>
<!-- Team Member 5 -->
<div class="bg-white p-6 rounded-lg shadow-md fade-in">
<img src="https://picsum.photos/150/150/" alt="Chris Brown" class="w-full h-48 object-cover rounded-t-lg">
<div class="mt-4">
<h4 class="text-xl font-semibold text-gray-900">{{name}}</h4>
<p class="text-gray-500">财务主管</p>
<p class="mt-2 text-base font-normal text-gray-700">管理公司财务事务,确保资金运作高效透明。</p>
</div>
</div>
<!-- Team Member 6 -->
{for{teamMembers}}
<div class="bg-white p-6 rounded-lg shadow-md fade-in" @click="add">
<img src="{{image}}" alt="Linda White" class="w-full h-48 object-cover rounded-t-lg">
<div class="mt-4">
<h4 class="text-xl font-semibold text-gray-900">{{name}}</h4>
<p class="text-gray-500">{{role}}</p>
<p class="mt-2 text-base font-normal text-gray-700">{{description}}</p>
</div>
</div>
{end{}}
</div>
</div>
</body>
<script type="module">
return {
data: (props) => {
console.log(props)
return {
a: 123,
name: 'vvv',
teamMembers: [
{
name: 'John',
role: '首席执行官',
description: '拥有超过15年的行业经验擅长战略规划和领导。',
image: 'https://picsum.photos/150/150/',
},
{
name: 'Doe',
role: '首席执行官',
description: '拥有超过15年的行业经验擅长战略规划和领导。',
image: 'https://picsum.photos/150/150/',
},
{
name: 'John Doe',
role: '首席执行官',
description: '拥有超过15年的行业经验擅长战略规划和领导。',
image: 'https://picsum.photos/150/150/',
},
]
}
},
methods: {
add() {
console.log(123)
}
},
}
</script>
</html>

@ -1,28 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
"*.html",
],
theme: {
extend: {
colors: {
vprimary: '#2196f3',
vsecondary: '#ecc94b',
vaccents: '#ff9800',
verror: '#f44336',
vwaring: '#ff5722',
vinfo: '#ffc107',
vsuccess: '#53de58',
vignore: '#d1d5db',
}
},
},
plugins: [],
}

@ -1,335 +0,0 @@
/*
* v.js
* Copyright (C) 2024 veypi <i@veypi.com>
*
* Distributed under terms of the GPL license.
*/
import { proxy } from './vyes.js'
(function() {
'use strict';
const config = { childList: true, subtree: true };
/**
* Represents a book.
* @param {Array} mutationsList
* @param {MutationObserver} observer
*/
const callback = function(mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
// console.log(mutation)
// console.log('A child node has been added or removed.');
}
}
};
const observer = new MutationObserver(callback);
var cacheUrl = {}
var pendingRequests = {};
async function fetchFileWithCache(url) {
if (cacheUrl[url]) {
return Promise.resolve(cacheUrl[url]);
}
if (pendingRequests[url]) {
return pendingRequests[url];
}
const promise = fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
})
.then(txt => {
cacheUrl[url] = txt;
return txt;
})
.catch(error => {
// console.error('Error fetching the file:', error);
delete pendingRequests[url];
throw error;
})
.finally(() => {
delete pendingRequests[url];
});
pendingRequests[url] = promise;
return promise;
}
function extractDoubleBraceContents(str) {
const regex = /{{|}}|{for{|{end{/g;
let depth = 0;
let currentIndex = 0
let results = [];
let match;
let flag = ''
while ((match = regex.exec(str)) !== null) {
if (match[0] === '{{') {
if (depth === 0) {
// 开始一个新的匹配
results.push([flag, str.slice(currentIndex, match.index)]);
currentIndex = match.index + 2
flag = 'val'
}
depth++;
} else if (match[0] === '}}') {
depth--;
if (depth === 0) {
results.push([flag, str.slice(currentIndex, match.index)]);
currentIndex = match.index + 2
flag = ''
}
} else if (match[0] === '{for{') {
if (depth === 0) {
// 开始一个新的匹配
results.push([flag, str.slice(currentIndex, match.index)]);
currentIndex = match.index + 5
flag = 'for'
}
depth++;
} else if (match[0] === '{end{') {
if (depth === 0) {
// 开始一个新的匹配
results.push([flag, str.slice(currentIndex, match.index)]);
currentIndex = match.index + 5
flag = 'end'
}
depth++;
}
}
results.push([flag, str.slice(currentIndex)]);
return results;
}
function extractBodyAndScript(htmlString) {
const bodyMatch = htmlString.match(/<body[^>]*>([\s\S]*)<\/body>/i);
const bodyContent = bodyMatch ? bodyMatch[1] : '';
const bodyItems = extractDoubleBraceContents(bodyContent);
let scriptMatches = htmlString.match(/<script[^>]*>([\s\S]*?)<\/script>/ig) || []
let scriptContents = []
let srcReg = /<script\s+[^>]*src=["']([^"']+)["'][^>]*>/
for (let s of scriptMatches) {
let match = srcReg.exec(s)
if (match && match.length) {
// handle src script
// console.log(match[1])
} else {
scriptContents.push(s.replace(/<script[^>]*>|<\/script>/ig, ''))
}
}
// 提取 <style> 内容
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
const styleMatches = [...htmlString.matchAll(styleRegex)];
const styleContents = styleMatches.map(match => match[1].trim());
// 解析 <style> 内容,按类名存储
//
const stylesBySelector = {};
if (styleContents.length > 0) {
styleContents.forEach(styleContent => {
const ruleRegex = /(@[\w-]+|[^{]+)\s*\{([\s\S]*?)\}/g;
let ruleMatch;
while ((ruleMatch = ruleRegex.exec(styleContent)) !== null) {
const selector = ruleMatch[1].trim();
const declarations = ruleMatch[2].trim();
if (selector.startsWith('@')) {
// 处理 @keyframes 等特殊规则
const keyframesName = selector.split(' ')[1];
stylesBySelector[keyframesName] = declarations;
} else {
// 处理普通选择器
stylesBySelector[selector] = declarations;
}
}
});
}
return {
bodyContent,
bodyItems,
scriptContents
};
}
/**
* @param{HTMLElement} targetDiv
* @param{HTMLElement} tmplDom
* */
function replaceDom(targetDiv, tmplDom) {
for (var i = 0; i < targetDiv.attributes.length; i++) {
var attr = targetDiv.attributes[i];
if (attr.name === 'class') {
tmplDom.className = tmplDom.className + ' ' + attr.value
} else {
tmplDom.setAttribute(attr.name, attr.value);
}
}
var parent = targetDiv.parentNode;
if (parent) {
parent.replaceChild(tmplDom, targetDiv);
}
}
/**
* @param{string} code
* @param{Object} data
* **/
function run(code, data) {
let args = '{' + Object.keys(data).join(',') + '}'
if (code.indexOf('\n') === -1) {
code = 'return ' + code
}
return new Function(args, code)(data)
}
/**
* @param{HTMLElement} node
* */
async function setupRef(node, data, itercount = 0) {
if (itercount > 100) {
log.warn('infinite loop')
return
}
const ref = node.getAttribute('ref') || ''
node.setAttribute('tag', ref)
node.removeAttribute('ref')
if (!ref) {
return
}
if (ref.endsWith('.js')) {
let mod = await import(ref)
let div = document.createElement('div')
replaceDom(node, mod.default)
node = mod.default
if (typeof mod.setup == 'function') {
let resp = mod.setup(node, data)
}
return
} else {
const txt = await fetchFileWithCache(ref + ".html")
proxy.DomListen(node, () => {
handleHtml(txt, node, data)
})
// let resp = new Function("{vnode,data}", ast.scriptContents.join('\n'))({ vnode: node, data: data })
}
for (let n of node.querySelectorAll("div[ref]")) {
// 确保子元素先加载完
// await setupRef(n, resp?.data || {}, itercount + 1)
}
}
function handleHtml(txt, node, data) {
let ast = extractBodyAndScript(txt)
let resp = new Function("", ast.scriptContents.join('\n'))()
if (resp && typeof resp.data == 'function') {
data = proxy.Watch(resp.data(data))
}
let bodyContent = ast.bodyContent
if (ast.bodyItems.length > 1) {
bodyContent = ''
console.log(ast.bodyItems)
let dataWrap = []
let tmpData = Object.assign({}, data)
let assign = true
let idx
for (idx = 0; idx < ast.bodyItems.length; idx++) {
let item = ast.bodyItems[idx]
if (!assign) {
assign = true
tmpData = Object.assign({}, data)
dataWrap.forEach((item) => {
if (item[1] >= 0) {
tmpData = Object.assign(tmpData, item[0][item[2]])
}
})
console.log(tmpData)
}
if (item[0] === 'val') {
let valr = run(item[1], tmpData)
bodyContent += valr
} else if (item[0] === '') {
bodyContent += item[1]
} else if (item[0] == 'for') {
dataWrap.push([run(item[1], tmpData), idx, 0])
assign = false
} else if (item[0] == 'end') {
let tdw = dataWrap[dataWrap.length - 1]
if (tdw[1] >= 0) {
if (tdw[2] < tdw[0].length - 1) {
tdw[2]++
idx = tdw[1]
} else {
dataWrap.pop()
}
}
assign = false
}
}
}
let div = document.createElement('div')
div.innerHTML = bodyContent
handleSlots(node, div)
// node.innerHTML = div.innerHTML
// let div = document.createElement('div')
replaceDom(node, div)
return resp
}
function handleSlots(origin, template) {
// handle slots
let originSnap = { '': [] }
let i = 0
let child
while (child = origin.children.item(i)) {
i++
let slotname = child.getAttribute ? child.getAttribute('slot') || '' : ''
if (slotname !== '') {
child.removeAttribute('slot')
originSnap[slotname] = child
} else {
originSnap[''].push(child)
}
}
let slots = template.querySelectorAll('slot')
slots.forEach((slot) => {
let slotname = slot.getAttribute('name') || ''
let snap = originSnap[slotname]
if (snap && snap.length !== 0) {
if (slotname === '') {
slot.replaceWith(...snap)
} else {
slot.replaceWith(snap)
}
} else {
slot.replaceWith(...slot.childNodes)
}
})
}
document.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded');
const node = document.getElementById('app');
// const node = document.body
observer.observe(node, config);
const sync = () => {
let test = node.querySelectorAll("div[ref]")
test.forEach(n => setupRef(n, {}))
}
sync()
// setInterval(sync, 10)
});
})();

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save