feat: oa fs doc

v3
veypi 3 weeks ago
parent 655af2fcb1
commit fe0f3b07d0

@ -6,13 +6,14 @@ import (
M "oa/models" M "oa/models"
"github.com/veypi/OneBD/rest" "github.com/veypi/OneBD/rest"
"github.com/veypi/utils"
"gorm.io/gorm" "gorm.io/gorm"
) )
func useApp(r rest.Router) { func useApp(r rest.Router) {
r.Delete("/:app_id", auth.Check("app", "app_id", auth.DoDelete), appDelete) 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", auth.Check("app", "app_id", auth.DoRead), appGet)
r.Get("/", auth.Check("app", "", auth.DoRead), appList) r.Get("/", appList)
r.Patch("/:app_id", auth.Check("app", "app_id", auth.DoUpdate), appPatch) r.Patch("/:app_id", auth.Check("app", "app_id", auth.DoUpdate), appPatch)
r.Post("/", auth.Check("app", "", auth.DoCreate), appPost) r.Post("/", auth.Check("app", "", auth.DoCreate), appPost)
} }
@ -50,20 +51,24 @@ func appList(x *rest.X) (any, error) {
M.App M.App
UserStatus string `json:"user_status"` UserStatus string `json:"user_status"`
}, 0, 10) }, 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") query := cfg.DB().Table("apps").Select("apps.*,app_users.status user_status")
uid := x.Request.Context().Value("uid").(string)
if opts.Name != nil { 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) 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 { } else {
query = query.Joins("LEFT JOIN app_users ON app_users.app_id = apps.id AND app_users.user_id = ?", uid) 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"). // logv.AssertError(cfg.DB().Table("accesses a").
// Select("a.name, a.t_id, a.level"). // 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). // Joins("INNER JOIN user_roles ur ON ur.role_id = a.role_id AND ur.user_id = ?", refresh.UID).
// Scan(&acList).Error) // Scan(&acList).Error)
err = query.Scan(&data).Error
return data, err return data, err
} }
@ -114,6 +119,7 @@ func appPost(x *rest.X) (any, error) {
data.Des = *opts.Des data.Des = *opts.Des
} }
err = cfg.DB().Transaction(func(tx *gorm.DB) error { err = cfg.DB().Transaction(func(tx *gorm.DB) error {
data.Key = utils.RandSeq(32)
err := tx.Create(data).Error err := tx.Create(data).Error
if err != nil { if err != nil {
return err return err

@ -2,6 +2,7 @@ package token
import ( import (
"encoding/hex" "encoding/hex"
"encoding/json"
"net/http" "net/http"
"oa/cfg" "oa/cfg"
"oa/errs" "oa/errs"
@ -50,13 +51,13 @@ func tokenPost(x *rest.X) (any, error) {
return nil, err return nil, err
} }
aid := cfg.Config.ID aid := cfg.Config.ID
if opts.AppID != nil { if opts.AppID != nil && *opts.AppID != "" {
aid = *opts.AppID aid = *opts.AppID
} }
data := &M.Token{} data := &M.Token{}
claim := &auth.Claims{} claim := &auth.Claims{}
claim.IssuedAt = jwt.NewNumericDate(time.Now()) claim.IssuedAt = jwt.NewNumericDate(time.Now())
claim.Issuer = "oa" claim.Issuer = cfg.Config.ID
if opts.Refresh != nil { if opts.Refresh != nil {
typ := "app" typ := "app"
if opts.Typ != nil { if opts.Typ != nil {
@ -74,22 +75,57 @@ func tokenPost(x *rest.X) (any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if typ == "app" { claim.AID = aid
if refresh.AID == aid {
// refresh token
claim.AID = refresh.AID
claim.UID = refresh.UID claim.UID = refresh.UID
claim.Name = refresh.Name claim.Name = refresh.Name
claim.Icon = refresh.Icon claim.Icon = refresh.Icon
claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10)) 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) acList := make(auth.Access, 0, 10)
logv.AssertError(cfg.DB().Table("accesses a"). logv.AssertError(cfg.DB().Table("accesses a").
Select("a.name, a.t_id, a.level"). 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). 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) Scan(&acList).Error)
claim.Access = acList claim.Access = acList
} else { 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 // 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" { } else if typ == "ufs" {
claim.AID = refresh.AID claim.AID = refresh.AID
@ -113,6 +149,8 @@ func tokenPost(x *rest.X) (any, error) {
http.SetCookie(x, cookie) http.SetCookie(x, cookie)
return token, nil return token, nil
} else {
return nil, errs.ArgsInvalid
} }
} else if opts.Code != nil && aid == cfg.Config.ID && opts.Salt != nil && opts.UserID != nil { } else if opts.Code != nil && aid == cfg.Config.ID && opts.Salt != nil && opts.UserID != nil {
// for oa login // for oa login
@ -131,11 +169,7 @@ func tokenPost(x *rest.X) (any, error) {
} }
data.UserID = *opts.UserID data.UserID = *opts.UserID
data.AppID = aid data.AppID = aid
if opts.ExpiredAt != nil {
data.ExpiredAt = *opts.ExpiredAt
} else {
data.ExpiredAt = time.Now().Add(time.Hour * 72) data.ExpiredAt = time.Now().Add(time.Hour * 72)
}
if opts.OverPerm != nil { if opts.OverPerm != nil {
data.OverPerm = *opts.OverPerm data.OverPerm = *opts.OverPerm
} }
@ -153,12 +187,10 @@ func tokenPost(x *rest.X) (any, error) {
if user.Nickname != "" { if user.Nickname != "" {
claim.Name = user.Nickname claim.Name = user.Nickname
} }
return auth.GenJwt(claim)
} else { } else {
return nil, errs.ArgsInvalid return nil, errs.ArgsInvalid
} }
token := logv.AssertFuncErr(auth.GenJwt(claim))
return token, err
} }
func tokenGet(x *rest.X) (any, error) { func tokenGet(x *rest.X) (any, error) {

@ -21,15 +21,15 @@ import (
) )
func GenJwt(claim *Claims) (string, error) { func GenJwt(claim *Claims) (string, error) {
return GenJwtWithKey(claim, cfg.Config.Key)
}
func GenJwtWithKey(claim *Claims, key string) (string, error) {
if claim.ExpiresAt == nil { if claim.ExpiresAt == nil {
claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Hour)) claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Hour))
} }
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
tokenString, err := token.SignedString([]byte(cfg.Config.Key)) return token.SignedString([]byte(key))
if err != nil {
return "", err
}
return tokenString, nil
} }
func ParseJwt(tokenString string) (*Claims, error) { func ParseJwt(tokenString string) (*Claims, error) {
@ -67,7 +67,6 @@ func CheckJWT(x *rest.X) (*Claims, error) {
return nil, err return nil, err
} }
x.Request = x.Request.WithContext(context.WithValue(x.Request.Context(), "uid", claims.UID))
return claims, nil return claims, nil
} }
@ -84,6 +83,7 @@ func Check(target string, pid string, l AuthLevel) func(x *rest.X) error {
if !claims.Access.Check(target, tid, l) { if !claims.Access.Check(target, tid, l) {
return errs.AuthNoPerm return errs.AuthNoPerm
} }
x.Request = x.Request.WithContext(context.WithValue(x.Request.Context(), "uid", claims.UID))
return nil return nil
} }
} }

@ -15,11 +15,11 @@ export interface App {
name: string name: string
icon: string icon: string
des: string des: string
participate: string
init_role_id?: string init_role_id?: string
init_role?: Role init_role?: Role
init_url: string init_url: string
user_count: number user_count: number
typ: string
status: string status: string
user_status: string user_status: string
} }

@ -28,7 +28,7 @@
left: 50%; left: 50%;
width: 0; width: 0;
height: 0.1em; height: 0.1em;
background-color: #000; background-color: rgba(0, 0, 0, 0.2);
transition: all 0.3s; transition: all 0.3s;
} }

@ -21,7 +21,6 @@ div[voa] {
.voa { .voa {
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
height: inherit; height: inherit;
@ -30,7 +29,10 @@ div[voa] {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
.voa-off {} .voa-off {
// color: var(--oafg-base);
font-size: 0.9rem;
}
.voa-on { .voa-on {
border-radius: 100%; border-radius: 100%;
@ -156,12 +158,12 @@ div[voa] {
} }
.voa-account { .voa-account {
padding: 0 1rem;
.voa-account-header { .voa-account-header {
display: flex; display: flex;
justify-content: space-between; justify-content: start;
align-items: center; align-items: center;
gap: 0.5rem;
} }
.voa-account-body { .voa-account-body {
@ -183,8 +185,10 @@ div[voa] {
} }
.voa-ab-info { .voa-ab-info {
height: 100%;
width: calc(100% - 6rem); width: calc(100% - 6rem);
display: flex; display: flex;
justify-content: space-between;
flex-direction: column; flex-direction: column;
div { div {
@ -196,23 +200,35 @@ div[voa] {
} }
} }
.voa-item-title { .voa-item {
padding: 0 1rem;
.voa-item-header {
display: flex;
margin: 0 -1rem;
height: 3rem; height: 3rem;
align-items: center;
.voa-item-title {
line-height: 3rem; line-height: 3rem;
font-size: 1.25rem; font-size: 1.25rem;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
font-weight: 500; font-weight: 500;
margin-left: -1rem;
} }
.voa-apps { }
padding: 0 1rem;
.voa-item-body {
margin: 0.5rem 0;
}
}
.voa-apps {
.voa-apps-header {}
.voa-apps-body { .voa-apps-body {
margin-top: 1rem;
display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem; gap: 1rem;
} }
@ -229,13 +245,8 @@ div[voa] {
} }
.voa-fs { .voa-fs {
padding: 0 1rem;
.voa-fs-header { .voa-fs-header {}
display: flex;
justify-content: space-between;
align-items: center;
}
.fsdir { .fsdir {
width: 100%; width: 100%;
@ -312,3 +323,17 @@ div[voa] {
opacity: 0.8; opacity: 0.8;
} }
} }
.vflex {
display: flex;
}
.vflex-center {
display: flex;
justify-content: center;
align-items: center;
}
.vflex-grow {
flex-grow: 1;
}

@ -7,8 +7,8 @@
import { v } from '../v2dom' import { v } from '../v2dom'
import logic from '../logic' import logic from '../logic'
export default v('voa-account', [ export default v('voa-account voa-item', [
v('voa-account-header', [ v('voa-account-header voa-item-header', [
v({ v({
class: 'voa-item-title', class: 'voa-item-title',
children: ['我的账户'], children: ['我的账户'],
@ -16,9 +16,12 @@ export default v('voa-account', [
logic.goto('/user') logic.goto('/user')
} }
}), }),
v('voa-ah-2')] v('vflex-grow'),
v('a', '🙍', { href: logic.urlwarp('/user') }),
v('a', '⚙️', { href: logic.urlwarp('/setting') }),
]
), ),
v('voa-account-body', [ v('voa-account-body voa-item-body', [
v('voa-ab-ico', [v({ typ: 'img', attrs: { 'src': () => `${logic.urlwarp(logic.user.icon)}` } })]), v('voa-ab-ico', [v({ typ: 'img', attrs: { 'src': () => `${logic.urlwarp(logic.user.icon)}` } })]),
v('voa-ab-info', [ v('voa-ab-info', [
v('voa-abi-1', [v('span', '昵称:'), v('span', () => logic.user.nickname)]), v('voa-abi-1', [v('span', '昵称:'), v('span', () => logic.user.nickname)]),

@ -17,9 +17,9 @@ export default () => {
apps.push(...e.filter((i) => i.user_status === 'ok')) apps.push(...e.filter((i) => i.user_status === 'ok'))
}) })
return v({ return v({
class: 'voa-apps', class: 'voa-apps voa-item',
children: [ children: [
v('voa-apps-header', v('voa-apps-header voa-item-header',
[v({ [v({
class: 'voa-item-title', class: 'voa-item-title',
children: ['我的应用'], children: ['我的应用'],
@ -28,7 +28,7 @@ export default () => {
} }
})] })]
), ),
v('voa-apps-body', [ v('voa-apps-body voa-item-body vflex', [
vfor(apps, vfor(apps,
(data) => v({ (data) => v({
class: 'voa-apps-box', class: 'voa-apps-box',

@ -13,10 +13,11 @@ import fstree from "./fstree";
export default () => { export default () => {
let fsdir = logic.token.oa.access?.Find('fs')?.tid || '/'
return v({ return v({
class: 'voa-fs', class: 'voa-fs voa-item',
children: [ children: [
v('voa-fs-header', v('voa-fs-header voa-item-header',
[ [
v({ v({
class: 'voa-item-title', class: 'voa-item-title',
@ -25,6 +26,7 @@ export default () => {
// logic.goto('/') // logic.goto('/')
} }
}), }),
v('vflex-grow'),
v({ v({
class: 'voa-fs-subtxt', children: '获取密钥', onclick: () => { class: 'voa-fs-subtxt', children: '获取密钥', onclick: () => {
api.token.Post({ api.token.Post({
@ -32,12 +34,17 @@ export default () => {
typ: 'ufs' typ: 'ufs'
}).then(e => { }).then(e => {
console.log(e) console.log(e)
navigator.clipboard.writeText(e).then(() => {
alert('文本已复制到剪贴板!');
}).catch(err => {
console.error('无法复制文本:', err);
});
}) })
} }
}) })
] ]
), ),
v('voa-fs-body', fstree('/')) v('voa-fs-body voa-item-body', fstree(fsdir))
] ]
}) })
} }

@ -37,7 +37,7 @@ const dirTree = (root: string | FileStat, depth = 0): HTMLElement => {
onclick: () => { onclick: () => {
api.token.Post({ refresh: logic.token.refresh.raw(), typ: 'ufs' }).then(e => { api.token.Post({ refresh: logic.token.refresh.raw(), typ: 'ufs' }).then(e => {
logic.goto(fs.user.urlwrap(item.filename), true) logic.goto(fs.user.urlwrap(item.filename), undefined, true)
}) })
} }
}) })

@ -35,7 +35,7 @@ export default class {
let frame_login = v({ let frame_login = v({
class: 'voa-off voa-hover-line-b', class: 'voa-off voa-hover-line-b',
vclass: [() => !logic.ready ? 'voa-scale-in' : 'voa-scale-off'], vclass: [() => !logic.ready ? 'voa-scale-in' : 'voa-scale-off'],
children: ['登录'], children: ['Sign in'],
onclick: () => { onclick: () => {
console.log('click login') console.log('click login')
bus.emit('login') bus.emit('login')

@ -170,11 +170,25 @@ const get_dav = (client: webdav.WebDAVClient, base_url: string) => {
} }
} }
const download = (url: string) => {
return new Promise<string>((resolve, reject) => {
fetch(url).then((response: any) => {
if (!response.ok) {
throw new Error(`Network response was not ok: ${response.statusText}`);
}
return response.text()
}).then(txt => {
resolve(txt)
}).catch(reject)
})
}
export default { export default {
user, user,
app, app,
download,
rename, rename,
} }

@ -11,7 +11,9 @@ import { proxy } from './v2dom'
class Token { class Token {
iat?: string iat?: string
// oa_id
iss?: string iss?: string
// token id
jti?: string jti?: string
exp?: number exp?: number
aid?: string aid?: string
@ -82,6 +84,11 @@ class Token {
}) })
} }
} }
function objectToUrlParams(obj: { [key: string]: any }) {
return Object.keys(obj).map(key => {
return encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]);
}).join('&');
}
// interface Token { // interface Token {
// aid: string // aid: string
@ -154,8 +161,15 @@ const logic = proxy.Watch({
Host() { Host() {
return logic.host return logic.host
}, },
urlwarp(url: string) { urlwarp(url: string, params?: { [key: string]: any }) {
let h = logic.host let h = logic.host
if (params) {
if (url.includes('?')) {
url += '&' + objectToUrlParams(params)
} else {
url += '?' + objectToUrlParams(params)
}
}
if (!url) { if (!url) {
return '' return ''
} }
@ -167,19 +181,23 @@ const logic = proxy.Watch({
} }
return h + url return h + url
}, },
goto(url: string, newtab = false) { goto(url: string, params?: { [key: string]: any }, newtab = false) {
if (newtab) { if (newtab) {
window.open(logic.urlwarp(url), '_blank'); window.open(logic.urlwarp(url, params), '_blank');
} else { } else {
window.location.href = logic.urlwarp(url) window.location.href = logic.urlwarp(url, params)
} }
}, },
}) })
bus.on('login', () => { bus.on('login', () => {
logic.goto('/login') logic.goto('/login', { redirect: window.location.href, uuid: logic.app_id })
}) })
bus.on('logout', () => { bus.on('logout', () => {
apitoken.value = ''
if (logic.token.refresh.jti) {
api.token.Delete(logic.token.refresh.jti)
}
logic.ready = false logic.ready = false
logic.token.refresh.clear() logic.token.refresh.clear()
logic.token.oa.clear() logic.token.oa.clear()

@ -21,6 +21,9 @@ export default new class {
private ui?: ui private ui?: ui
constructor() { constructor() {
} }
logic() {
return logic
}
local() { local() {
return logic.user return logic.user
} }
@ -30,13 +33,18 @@ export default new class {
fs() { fs() {
return fs return fs
} }
init(host?: string, code?: string) { init(host?: string, aid?: string, code?: string) {
if (host) { if (host) {
logic.host = host logic.host = host
set_base_host(host) set_base_host(host)
} }
if (aid) {
logic.app_id = aid
}
if (code) { if (code) {
logic.token.refresh.set(code) logic.token.refresh.set(code)
logic.token.app.clear()
logic.token.oa.clear()
} }
return logic.init() return logic.init()
} }
@ -60,6 +68,12 @@ export default new class {
logout() { logout() {
bus.emit('logout') bus.emit('logout')
} }
urlwarp(url: string, params?: { [key: string]: any }) {
return logic.urlwarp(url, params)
}
goto(url: string, params?: { [key: string]: any }, newtab = false) {
return logic.goto(url, params, newtab)
}
on(evt: string, fn: (d?: any) => void) { on(evt: string, fn: (d?: any) => void) {
bus.on(evt, fn) bus.on(evt, fn)
} }
@ -73,6 +87,9 @@ export default new class {
Get: api.app.Get, Get: api.app.Get,
List: api.app.List, List: api.app.List,
}, },
token: {
Post: api.token.Post
}
} }
} }
Token() { Token() {

@ -93,10 +93,14 @@ export class auths {
} }
return new authLevel(l) return new authLevel(l)
} }
Find(name: string): modelsSimpleAuth | undefined {
return this.list.find(i => i.name === name)
}
} }
export interface Auths { export interface Auths {
Get(name: string, rid: string): authLevel Get(name: string, rid: string): authLevel
Find(name: string): modelsSimpleAuth | undefined
} }

@ -6,10 +6,20 @@ import './style.css'
import oaer from '../lib/main' import oaer from '../lib/main'
let code = let code =
`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI3ODA5YTZlNjhmNDE0OWM5ODFhMmFhNTI0NTE0MzIyZiIsImFpZCI6InRNUnhXejc3UDlBQk5aQTNaSXVvTlFJTGpWQkJJVWRmIiwibmFtZSI6ImFkbWluIiwiaWNvbiI6Imh0dHBzOi8vcHVibGljLnZleXBpLmNvbS9pbWcvYXZhdGFyLzAwNTEuanBnIiwiYWNjZXNzIjpudWxsLCJpc3MiOiJvYSIsImV4cCI6MTczMDk4NDY4MiwiaWF0IjoxNzMwNzI1NDgyLCJqdGkiOiIxOWZlZTc4YjQwN2M0ZTQ5OWI1Yjg2YmJjNTNjMTA2YyJ9.vIriE-L2AZLtigWXAXHrTG2_XIELqp0bAnFEBX0Hw8w` `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiI3ODA5YTZlNjhmNDE0OWM5ODFhMmFhNTI0NTE0MzIyZiIsImFpZCI6InRNUnhXejc3UDlBQk5aQTNaSXVvTlFJTGpWQkJJVWRmIiwibmFtZSI6ImFkbWluIiwiaWNvbiI6Imh0dHBzOi8vcHVibGljLnZleXBpLmNvbS9pbWcvYXZhdGFyLzAwNTEuanBnIiwiYWNjZXNzIjpudWxsLCJpc3MiOiJvYSIsImV4cCI6MTczMDk4NDY4MiwiaWF0IjoxNzMwNzI1NDgyLCJqdGkiOiIxOWZlZTc4YjQwN2M0ZTQ5OWI1Yjg2YmJjNTNjMTA2YyJ9.vIriE-L2AZLtigWXAXHrTG2_XIELqp0bAnFEBX0Hw8w`
oaer.init('http://localhost:4000', code).then(e => { let token = ''
let querys = window.location.search.match(/token=([^&]*)/)
if (querys && querys.length > 1) {
token = decodeURIComponent(querys[1])
}
console.log(token)
oaer.init('http://localhost:3000', '149f48cfc9f24549a4a0269547a64db5', token).then(e => {
console.log(`login ${e.name}`) console.log(`login ${e.name}`)
}).catch(() => { }).catch(() => {
console.log('not login') console.log('not login')
}).finally(() => {
if (token) {
window.location.search = window.location.search.replace(/token=([^&]*)/, '')
}
}) })
oaer.render_ui('voa') oaer.render_ui('voa')

@ -4,7 +4,22 @@
* 2024-05-31 18:10 * 2024-05-31 18:10
* Distributed under terms of the MIT license. * Distributed under terms of the MIT license.
*/ */
import oaer from '@veypi/oaer'
const ready = ref(false)
oaer.init().then(() => {
api.apitoken.value = oaer.Token()
api.apitoken.set_updator(oaer.TokenRefresh)
console.log('oaer init')
ready.value = true
oaer.on('logout', () => {
api.apitoken.value = ''
ready.value = false
})
}).catch(() => {
console.log('oaer init error')
oaer.logout()
})
export default defineAppConfig({ export default defineAppConfig({
// host: window.location.protocol + '//' + window.location.host, // host: window.location.protocol + '//' + window.location.host,
@ -12,8 +27,8 @@ export default defineAppConfig({
// primary: '#2196f3', // primary: '#2196f3',
// gray: '#111' // gray: '#111'
}, },
host: '', ready: computed(() => ready),
id: 'tMRxWz77P9ABNZA3ZIuoNQILjVBBIUdf', host: window.location.origin,
layout: { layout: {
theme: '', theme: '',
fullscreen: false, fullscreen: false,

@ -37,6 +37,10 @@ function Go() {
router.push('/app/' + props.core.id) router.push('/app/' + props.core.id)
return return
} }
if (!oaer.isValid()) {
oaer.login()
return
}
// $q.dialog({ // $q.dialog({
// title: '', // title: '',
// message: ' ' + props.core.name, // message: ' ' + props.core.name,

@ -10,18 +10,24 @@
<div class="ico" @click="router.push('/')"></div> <div class="ico" @click="router.push('/')"></div>
<div>OneAuth</div> <div>OneAuth</div>
<div class="grow"></div> <div class="grow"></div>
<OneIcon class="mx-2" @click="toggle_lang" :name="$i18n.locale !== 'zh-CN' ? 'in-Zh_Cn' : 'in-en'"></OneIcon> <OneIcon class="mx-2 cursor-pointer" @click="oaer.goto('/docs')" name="help" />
<OneIcon class="mx-2" @click="toggle_fullscreen" :name="app.layout.fullscreen ? 'compress' : 'expend'"></OneIcon> <OneIcon class="mx-2 cursor-pointer" @click="toggle_lang"
<OneIcon class="mx-2" @click="toggle_theme" :name="app.layout.theme === '' ? 'light' : 'dark'"></OneIcon> :name="$i18n.locale !== 'zh-CN' ? 'in-Zh_Cn' : 'in-en'" />
<OneIcon class="mx-2 cursor-pointer" @click="toggle_fullscreen"
:name="app.layout.fullscreen ? 'compress' : 'expend'" />
<OneIcon class="mx-2 cursor-pointer" @click="toggle_theme" :name="app.layout.theme === '' ? 'light' : 'dark'">
</OneIcon>
<div class="mr-4 ml-2" id='oaer'></div> <div class="mr-4 ml-2" id='oaer'></div>
</div> </div>
<div class="menu"> <div class="page-body">
<div v-if="menusCount" class="menu">
<Menu :show_name="menu_mode === 2"></Menu> <Menu :show_name="menu_mode === 2"></Menu>
</div> </div>
<div class="menu-hr"></div> <div v-if="menusCount" class="menu-hr"></div>
<div class="main px-8 py-6"> <div class="main px-8 py-6">
<slot /> <slot />
</div> </div>
</div>
<div class="footer flex justify-around items-center"> <div class="footer flex justify-around items-center">
<div @click="util.goto('https://veypi.com')">© 2024 veypi</div> <div @click="util.goto('https://veypi.com')">© 2024 veypi</div>
<div>使用说明</div> <div>使用说明</div>
@ -34,24 +40,14 @@
import { OneIcon } from '@veypi/one-icon' import { OneIcon } from '@veypi/one-icon'
import oaer from '@veypi/oaer' import oaer from '@veypi/oaer'
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { apitoken } from '@veypi/oaer/lib/api';
let i18n = useI18n() let i18n = useI18n()
let app = useAppConfig() let app = useAppConfig()
let router = useRouter() let router = useRouter()
const colorMode = useColorMode() const colorMode = useColorMode()
app.host = window.location.protocol + '//' + window.location.host let menu_handler = useMenuStore()
api.apitoken.value = oaer.Token() let menusCount = computed(() => menu_handler.menus.length)
api.apitoken.set_updator(oaer.TokenRefresh)
oaer.init(app.host).then((e) => {
oaer.render_ui('oaer')
}).catch(() => {
oaer.logout()
})
oaer.on('logout', () => {
useRouter().push('/login')
})
let menu_mode = ref(1) let menu_mode = ref(1)
let toggle_menu = (m: 0 | 1 | 2) => { let toggle_menu = (m: 0 | 1 | 2) => {
@ -123,12 +119,16 @@ onMounted(() => {
} }
} }
.page-body {
display: flex;
height: calc(100vh - v-bind('app.layout.header_height + app.layout.footer_height + "px"'));
.menu { .menu {
overflow: hidden; overflow: hidden;
vertical-align: top; vertical-align: top;
display: inline-block; display: inline-block;
width: v-bind("app.layout.menu_width + 'px'"); width: v-bind("app.layout.menu_width + 'px'");
height: calc(100vh - v-bind('app.layout.header_height + app.layout.footer_height + "px"')); height: 100%;
transition: width 0.3s linear; transition: width 0.3s linear;
} }
@ -136,18 +136,20 @@ onMounted(() => {
vertical-align: top; vertical-align: top;
display: inline-block; display: inline-block;
width: 1px; width: 1px;
height: calc(100vh - v-bind('app.layout.header_height + app.layout.footer_height + "px"')); height: 100%;
background: #999; background: #999;
} }
.main { .main {
flex-grow: 1;
vertical-align: top; vertical-align: top;
display: inline-block; display: inline-block;
overflow: auto; overflow: auto;
width: calc(100vw - v-bind("app.layout.menu_width + 1 + 'px'")); height: 100%;
height: calc(100vh - v-bind('app.layout.header_height + app.layout.footer_height + "px"'));
transition: width 0.3s linear; transition: width 0.3s linear;
} }
}
.footer { .footer {
height: v-bind("app.layout.footer_height + 'px'"); height: v-bind("app.layout.footer_height + 'px'");

@ -0,0 +1,31 @@
/*
* auth.ts
* Copyright (C) 2024 veypi <i@veypi.com>
* 2024-11-05 10:34
* Distributed under terms of the GPL license.
*/
import oaer from '@veypi/oaer'
oaer.on('logout', () => {
let r = useRoute()
console.log(r.path)
if (r.path === now_auth && r.path !== '/') {
oaer.goto('/')
}
})
let now_auth = ''
export default defineNuxtRouteMiddleware((to, from) => {
console.log(to.path)
if (!oaer.isValid() && to.path !== '/login') {
return navigateTo('/login')
}
now_auth = to.path
// if (to.params.id === '1') {
// return abortNavigation()
// }
// 在实际应用中,你可能不会将每个路由重定向到 `/`
// 但是在重定向之前检查 `to.path` 是很重要的,否则可能会导致无限重定向循环
// if (to.path !== '/') {
// return navigateTo('/')
// }
})

@ -49,7 +49,7 @@ export default defineNuxtConfig({
{ children: 'JavaScript is required' } { children: 'JavaScript is required' }
], ],
link: [ link: [
{ rel: 'icon', type: 'image/ico', href: 'favicon.ico' } { rel: 'icon', type: 'image/ico', href: 'favicon.svg' }
], ],
script: [ script: [
{ src: '/icon.js' }, { src: '/icon.js' },

@ -20,6 +20,10 @@
import type { models } from '#imports'; import type { models } from '#imports';
import msg from '@veypi/msg'; import msg from '@veypi/msg';
definePageMeta({
middleware: ['auth']
})
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()

@ -0,0 +1,64 @@
<!--
* [...furl].vue
* Copyright (C) 2024 veypi <i@veypi.com>
* 2024-11-05 18:08
* Distributed under terms of the GPL license.
-->
<template>
<div class="w-full h-full">
<!-- <h1 class="page-h1">文档中心</h1> -->
<!-- <q-inner-loading :showing="!visible" label="Please wait..." label-class="text-teal" label-style="font-size: 1.1em" /> -->
<div class="w-full px-8">
<Editor v-if='doc' eid='doc' preview :content="doc"></Editor>
</div>
</div>
</template>
<script lang="ts" setup>
import msg from '@veypi/msg';
import { fs } from '@veypi/oaer';
let doc = ref('')
let route = useRoute()
let router = useRouter()
let typ = computed(() => route.params.typ)
let furl = computed(() => {
let f = route.params.furl
if (typ.value === 'pub') {
return '/doc/' + f
}
return f
})
const visible = ref(false)
const render = (url: string) => {
console.log(url)
if (!url) {
return
}
if (typ.value === 'pub') {
fs.download(url).then(t => {
doc.value = t
visible.value = true
}).catch(e => {
console.warn(e)
msg.Warn('访问文档地址不存在')
router.back()
})
}
}
watch(furl, (u, o) => {
if (u && u !== o) {
render(u as string)
}
}, { immediate: true })
onMounted(() => {
})
</script>
<style scoped></style>

@ -5,11 +5,62 @@
* Distributed under terms of the MIT license. * Distributed under terms of the MIT license.
--> -->
<template> <template>
<div>docs</div> <div>
<h1 class="page-h1">文档中心</h1>
<div class="mx-8 mt-10">
<template v-for="(doc, i) in Docs" :key="i">
<div class="mb-10">
<div class="text-xl flex items-center mb-4">
<OneIcon class="mx-2" :name="doc.icon"></OneIcon>
<span>{{ doc.name }}</span>
</div>
<div class="flex gap-8 ml-10">
<template v-for="item in doc.items" :key="item.name">
<div class="doc-item border-2 rounded-md px-4 py-2" clickable outline
@click="$router.push('/doc_pub/' + item.url)" icon="bookmark">
<span>{{ item.name }}</span>
</div>
</template>
</div>
<UDivider class="mt-6" label="OR" size="sm" />
</div>
</template>
</div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { models } from '#imports';
const Docs = ref<models.DocGroup[]>([
{
name: '用户',
icon: 'team',
items: [
{ name: '用户注册授权过程', url: '' },
{ name: '用户角色与权限', url: '' },
{ name: 'api文档', url: '' }
]
},
{
name: "应用",
icon: 'apps',
items: [
{ name: '应用创建及基本设置', url: '' },
{ name: '应用权限设置', url: '' },
{ name: '应用对接oa流程', url: '' },
],
},
{
name: "系统使用",
icon: "setting",
items: [
{ name: "编辑器使用及语法", url: 'markdown.md' }
]
}
])
</script> </script>
<style scoped></style> <style scoped>
.doc-item {}
</style>

@ -7,7 +7,7 @@
<template> <template>
<div> <div>
<div v-if="ofApps.length > 0"> <div class="mb-20" v-if="ofApps.length > 0 && ready">
<div class="flex justify-between"> <div class="flex justify-between">
<h1 class="page-h1">{{ $t('c.myapps') }}</h1> <h1 class="page-h1">{{ $t('c.myapps') }}</h1>
<div class="my-5 mr-10"> <div class="my-5 mr-10">
@ -22,7 +22,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="mt-20" v-if="apps.length > 0"> <div class="" v-if="apps.length > 0">
<h1 class="page-h1">{{ $t('c.app store') }}</h1> <h1 class="page-h1">{{ $t('c.app store') }}</h1>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
<div v-for="(item, k) in apps" class="flex items-center justify-center" :key="k"> <div v-for="(item, k) in apps" class="flex items-center justify-center" :key="k">
@ -65,6 +65,8 @@ import msg from '@veypi/msg';
import oaer from '@veypi/oaer'; import oaer from '@veypi/oaer';
let ready = useAppConfig().ready
let allApps = ref<models.App[]>([]); let allApps = ref<models.App[]>([]);
let apps = computed(() => allApps.value.filter(e => e.user_status !== 'ok')) let apps = computed(() => allApps.value.filter(e => e.user_status !== 'ok'))
@ -77,6 +79,7 @@ function getApps() {
} }
); );
} }
watch(() => ready.value, getApps)
let new_flag = ref(false); let new_flag = ref(false);

@ -47,6 +47,64 @@
</button> </button>
</div> </div>
</div> </div>
<div class="login content" v-else-if='isValid'>
<div class="flex mt-10 h-full" v-if="app.id">
<div class="flex flex-col items-center w-1/2 justify-center">
<img class="rounded-full h-44 w-44" :src="oaer.local().icon">
<div class="mt-4 text-2xl">{{ oaer.local().nickname || oaer.local().username }}</div>
<div class="mt-4"></div>
</div>
<div class="flex flex-col w-1/2 gap-4">
<div class="flex items-center justify-start gap-4">
<img class="rounded-full h-16 w-16" :src="app.icon">
<div>{{ app.name }}</div>
</div>
<div class="flex-grow">
<div>您正在授权登录 <span class="font-bold text-xl">{{ app.name }}</span> </div>
<div class="mt-8 ml-8 flex flex-col gap-4">
<div class="auth-line">
<UToggle color="primary" :model-value="true" disabled />
<div class='auth-info'>Basic User Info</div>
</div>
<div class="auth-line">
<UToggle color="primary" v-model="app_perm.fs[0]" />
<div class='auth-info flex'>
<UInput v-if="app_perm.fs[0]" :padded="false" v-model="app_perm.fs[1]"
placeholder="userfile auth scope" variant="none" class="w-full border-b-black border-b-2" />
<span v-else>userfile permission</span>
</div>
</div>
</div>
</div>
<div class="flex">
<button style="" @click="signout" class='ok back voa-btn'>
Sign out
</button>
<button @click="redirect()" class='ok voa-btn'>
Sign in
</button>
</div>
<div class="text-sm text-gray-600 text-center">
Authorizing will redirect to {{ app.init_url }}
</div>
</div>
</div>
<div class="flex mt-10 h-full justify-center items-center flex-col" v-else>
<img class="rounded-full h-44 w-44" :src="oaer.local().icon">
<div class="mt-4 text-2xl">{{ oaer.local().nickname || oaer.local().username }}</div>
<div class="flex-grow"></div>
<div class="flex w-1/2">
<button style="" @click="signout" class='ok back voa-btn'>
Sign out
</button>
<button @click="redirect()" class='ok voa-btn'>
Sign in
</button>
</div>
</div>
</div>
<div class="login content flex flex-col justify-between" v-else> <div class="login content flex flex-col justify-between" v-else>
<div :check="checks.u" class="username mt-8"> <div :check="checks.u" class="username mt-8">
<input @change="check" v-model="data.username" autocomplete="username" <input @change="check" v-model="data.username" autocomplete="username"
@ -56,8 +114,8 @@
<input @change="check" v-model="data.password" autocomplete="password" type='password' <input @change="check" v-model="data.password" autocomplete="password" type='password'
placeholder="password"> placeholder="password">
</div> </div>
<button @click="login" class='ok voa-btn'> <button @click="signin" class='ok voa-btn'>
login Sign in
</button> </button>
<div class="last"> <div class="last">
<div class="icos"> <div class="icos">
@ -74,36 +132,23 @@
</Transition> </Transition>
</div> </div>
</div> </div>
<!-- <div class="h-full w-full flex items-center justify-center"> -->
<!-- <div class="px-10 pb-9 pt-16 rounded-xl w-96 bg-gray-50 relative"> -->
<!-- <img class='vico' :src="'/favicon.ico'"> -->
<!-- <Vinput class="mb-8" v-model="data.username" label="用户名" :validate="" /> -->
<!-- <Vinput class='mb-8' v-model="data.password" label='密码' :validate="" type="password" /> -->
<!-- <div class="flex justify-around mt-4"> -->
<!-- <div class='vbtn bg-green-300' @click="$router.push({ -->
<!-- name: -->
<!-- 'register' -->
<!-- })">注册</div> -->
<!-- <div class='vbtn bg-green-300' @click="onSubmit"></div> -->
<!-- <div class='vbtn bg-gray-300' @click="onReset"> </div> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import msg from '@veypi/msg'; import msg from '@veypi/msg';
import * as crypto from 'crypto-js' import * as crypto from 'crypto-js'
import oaer from '@veypi/oaer' import oaer from '@veypi/oaer'
import type { models } from '#imports';
definePageMeta({ definePageMeta({
layout: false, layout: false,
}) })
const app = useAppConfig()
const route = useRoute() const route = useRoute()
const router = useRouter() const isValid = ref(oaer.isValid())
const app = ref<models.App>({} as models.App)
const app_perm = ref<{ [key: string]: [boolean, string, number] }>({ 'fs': [true, '/', 4], 'app': [true, '', 1], 'user': [true, '', 1] })
let data = ref({ let data = ref({
@ -113,7 +158,6 @@ let data = ref({
}) })
let uReg = /^[\w]{5,}$/ let uReg = /^[\w]{5,}$/
let pReg = /^[\w@_#]{6,}$/ let pReg = /^[\w@_#]{6,}$/
let checks = ref({ 'u': true, 'p': true, 'p2': true }) let checks = ref({ 'u': true, 'p': true, 'p2': true })
@ -134,7 +178,11 @@ function deriveKey(password: string, salt: any) {
}) })
} }
const login = () => { const signout = () => {
oaer.logout()
isValid.value = false
}
const signin = () => {
enable_check.value = true enable_check.value = true
check() check()
if (!checks.value.u || !checks.value.p) { if (!checks.value.u || !checks.value.p) {
@ -155,7 +203,7 @@ const login = () => {
user_id: id, code: p.toString(), salt: user_id: id, code: p.toString(), salt:
salt.toString() salt.toString()
}).then(e => { }).then(e => {
oaer.init(app.host, e).then(() => { oaer.init('', '', e).then(() => {
redirect("") redirect("")
}).catch((e) => { }).catch((e) => {
console.warn(e) console.warn(e)
@ -171,32 +219,6 @@ const login = () => {
console.warn(e) console.warn(e)
} }
}) })
// api.user.salt(data.value.username).then((e: any) => {
// let id = e.data.id
// let key = deriveKey(data.value.password, e.data.salt)
// let salt = crypto.lib.WordArray.random(128 / 8)
// let opts = {
// iv: salt,
// mode: crypto.mode.CBC,
// padding: crypto.pad.Pkcs7
// }
// let p = crypto.AES.encrypt(id, key, opts)
// api.user.token(id, p.toString(), salt.toString()).then(e => {
// console.log(e)
// })
// })
// api.user.login(data.value.username,
// { client: 'vvv', typ: 'sss', pwd: Base64.encodeURL(data.value.password) }).then((data: any) => {
// util.setToken(data.auth_token)
// // msg.Info('')
// // user.fetchUserData()
// let url = route.query.redirect || data.redirect || ''
// console.log([url])
// redirect(url)
// }).catch(e => {
// msg.Warn(e)
// })
} }
const register = () => { const register = () => {
enable_check.value = true enable_check.value = true
@ -223,26 +245,36 @@ const reset = () => {
check() check()
} }
let uuid = computed(() => {
return route.query.uuid
})
let ifLogOut = computed(() => {
return route.query.logout === '1'
})
let aOpt = ref('' as '' | 'newbie' | 'oh_no') let aOpt = ref('' as '' | 'newbie' | 'oh_no')
function redirect(url?: string) {
function redirect(url: string) {
if (url === 'undefined') { if (url === 'undefined') {
url = '' url = ''
} }
if (uuid.value && uuid.value !== app.id) { if (route.query.redirect) {
api.app.Get(uuid.value as string).then((app) => { url = route.query.redirect as string
api.token.Post({ }
refresh: '', let uuid = route.query.uuid as string
user_id: uuid.value as string, if (uuid) {
app_id: uuid.value as string, oaer.api().app.Get(uuid as string).then((app) => {
if (uuid === oaer.logic().oa_id) {
oaer.goto(url || app.init_url || '/')
} else {
let perm = []
for (let i in app_perm.value) {
let p = app_perm.value[i]
if (p[0]) {
perm.push({
name: i,
tid: p[1],
level: p[2]
})
}
}
oaer.api().token.Post({
refresh: oaer.logic().token.refresh.raw(),
app_id: uuid,
over_perm: JSON.stringify(perm)
}).then(e => { }).then(e => {
url = url || app.init_url url = url || app.init_url
// let data = JSON.parse(Base64.decode(e.split('.')[1])) // let data = JSON.parse(Base64.decode(e.split('.')[1]))
@ -250,42 +282,31 @@ function redirect(url: string) {
e = encodeURIComponent(e) e = encodeURIComponent(e)
if (url.indexOf('$token') >= 0) { if (url.indexOf('$token') >= 0) {
url = url.replaceAll('$token', e) url = url.replaceAll('$token', e)
} else {
url = buildURL(url, 'token=' + e)
} }
window.location.href = url oaer.goto(url, { 'token': e })
}) })
}
}) })
} else if (url) { } else if (url) {
router.push(url) oaer.goto(url)
} else { } else {
router.push('/') oaer.goto('/')
}
} }
function buildURL(url: string, params?: string) {
if (!params) {
return url;
}
// params
var hashmarkIndex = url.indexOf('#');
if (hashmarkIndex !== -1) {
url = url.slice(0, hashmarkIndex);
} }
url += (url.indexOf('?') === -1 ? '?' : '&') + params; onMounted(() => {
let uuid = route.query.uuid as string
return url; if (isValid.value && uuid) {
oaer.api().app.Get(uuid).then(e => {
app.value = e
}).catch(e => {
if (e.code === 40401) {
msg.Warn('参数错误: 该应用不存在')
return
} }
console.warn(e)
})
onMounted(() => {
if (ifLogOut.value) {
util.setToken('')
} else if (util.checkLogin()) {
console.log(util.checkLogin())
redirect(route.query.redirect as string || '')
} }
}) })
</script> </script>
@ -303,6 +324,13 @@ onMounted(() => {
/* backdrop-filter: blur(5px); */ /* backdrop-filter: blur(5px); */
} }
.auth-line {
display: flex;
gap: 1rem;
.auth-info {}
}
.box { .box {
user-select: none; user-select: none;
position: sticky; position: sticky;

@ -0,0 +1,824 @@
[[toc]]
# 例子
# Markdown { 简明手册 | jiǎn míng shǒu cè }
# 基本语法
---
## 字体样式
**说明**
- 使用`*(或_)` 和 `**(或__)` 表示*斜体*和 **粗体**
- 使用 `/` 表示 /下划线/ ,使用`~~` 表示~~删除线~~
- 使用`^(或^^)`表示^上标^或^^下标^^
- 使用 ! 号+数字 表示字体 !24 大! !12 小! [^专有语法提醒]
- 使用两个(三个)!号+RGB 颜色 表示!!#ff0000 字体颜色!!(!!!#f9cb9c 背景颜色!!!)[^专有语法提醒]
**示例**
```
[!!#ff0000 红色超链接!!](http://www.google.com)
[!!#ffffff !!!#000000 黑底白字超链接!!!!!](http://www.google.com)
[新窗口打开](http://www.google.com){target=_blank}
鞋子 !32 特大号!
大头 ^`儿子`^ 和小头 ^^`爸爸`^^
爱在~~西元前~~**当下**
```
**效果**
[!!#ff0000 红色超链接!!](http://www.google.com)
[!!#ffffff !!!#000000 黑底白字超链接!!!!!](http://www.google.com)
[新窗口打开](http://www.google.com){target=_blank}
鞋子 !32 特大号!
大头 ^`儿子`^ 和小头 ^^`爸爸`^^
爱在~~西元前~~**当下**
---
## 标题设置
**说明**
- 在文字下方加 === 可使上一行文字变成一级标题
- 在文字下方加 --- 可使上一行文字变成二级标题
- 在行首加井号(#)表示不同级别的标题,例如:# H1, ##H2, ###H3
---
## 超链接
**说明**
- 使用 `[描述](URL)` 为文字增加外链接
- 使用`<URL>`插入一个链接
- URL 会自动转成链接
**示例**
```
这是 [Google](https://www.google.com) 的链接。
这是 [一个引用的][引用一个链接] 的链接。
这是一个包含中文的链接<https://www.google.com?param=中文>,中文
直接识别成链接https://www.google.com?param=中文,中文 用空格结束
[引用一个链接]
[引用一个链接]: https://www.google.com
```
**效果**
这是 [Google](https://www.google.com) 的链接。
这是 [一个引用的][引用一个链接] 的链接。
这是一个包含中文的链接<https://www.google.com?param=中文>,中文
直接识别成链接https://www.google.com?param=中文,中文 用空格结束
[引用一个链接]
[引用一个链接]: https://www.google.com
---
## 无序列表
**说明**
- 在行首使用 \*+- 表示无序列表
**示例**
```
- 无序列表项 一`默认`
- 无序列表项 二
- 无序列表2.1
- 无序列表2.2
- 无序列表项 三
+ 无序列表3.1`空心圆`
+ 无序列表3.1
- 无序列表四
* 无序列表4.1`实心方块`
* 无序列表4.2
```
**效果**
- 无序列表项 一`默认`
- 无序列表项 二
- 无序列表2.1
- 无序列表2.2
- 无序列表项 三
+ 无序列表3.1`空心圆`
+ 无序列表3.1
- 无序列表四
* 无序列表4.1`实心方块`
* 无序列表4.2
---
## 有序列表
**说明**
- 在行首使用数字、字母、汉字和点表示有序列表
**示例**
```
1. 有序列表项 一`阿拉伯数字`
1. 有序列表项 二
I. 有序列表项 2.1`罗马数字`
I. 有序列表项 2.2
I. 有序列表项 2.3
1. 有序列表 三
a. 有序列表3.1`希腊字母`
a. 有序列表3.2
a. 有序列表3.3
1. 有序列表 四
一. 有序列表4.1`中文数字`
一. 有序列表4.2
一. 有序列表4.3
```
**效果**
1. 有序列表项 一`阿拉伯数字`
1. 有序列表项 二
I. 有序列表项 2.1`罗马数字`
I. 有序列表项 2.2
I. 有序列表项 2.3
1. 有序列表 三
a. 有序列表3.1`希腊字母`
a. 有序列表3.2
a. 有序列表3.3
1. 有序列表 四
一. 有序列表4.1`中文数字`
一. 有序列表4.2
一. 有序列表4.3
---
## 引用
**说明**
- 在行首使用 > 表示文字引用
**示例**
```
> 野火烧不尽,春风吹又生
```
**效果**
> 野火烧不尽,春风吹又生
---
## 行内代码
**说明**
- 使用 \`代码` 表示行内代码
**示例**
```
让我们聊聊 `html`
```
**效果**
让我们聊聊 `html`
---
## 代码块
**说明**
- 使用 三个` 表示代码块
**效果**
```
这是一个代码块
有两行
```
---
## 插入图像
**说明**
- 使用 `![描述](图片链接地址)` 插入图像
- 截图在编辑器中粘贴ctrl+V也可以插入图像
- 使用`![描述#宽度#高度#对齐方式](图片链接地址)` 可以调整图片大小[^专有语法提醒]
**示例**
```
标准图片 ![一条dog#100px](/cherry/images/demo-dog.png)
设置图片大小(相对大小&绝对大小) ![一条dog#10%#50px](/cherry/images/demo-dog.png)
设置图片对齐方式:
**左对齐+边框**
![一条dog#auto#100px#left#border](/cherry/images/demo-dog.png)
**居中+边框+阴影**
![一条dog#auto#100px#center#B#shadow](/cherry/images/demo-dog.png)
**右对齐+边框+阴影+圆角**
![一条dog#auto#100px#right#B#S#radius](/cherry/images/demo-dog.png)
**浮动左对齐+边框+阴影+圆角**
![一条dog#auto#100px#float-left#B#S#R](/cherry/images/demo-dog.png)
开心也是一天,不开心也是一天
这样就过了两天,汪
```
**效果**
标准图片 ![一条dog#100px](/cherry/images/demo-dog.png)
设置图片大小(相对大小&绝对大小) ![一条dog#10%#50px](/cherry/images/demo-dog.png)
设置图片对齐方式:
**左对齐+边框**
![一条dog#auto#100px#left#border](/cherry/images/demo-dog.png)
**居中+边框+阴影**
![一条dog#auto#100px#center#B#shadow](/cherry/images/demo-dog.png)
**右对齐+边框+阴影+圆角**
![一条dog#auto#100px#right#B#S#radius](/cherry/images/demo-dog.png)
**浮动左对齐+边框+阴影+圆角**
![一条dog#auto#100px#float-left#B#S#R](/cherry/images/demo-dog.png)
开心也是一天,不开心也是一天
这样就过了两天,汪
> 属性释义:
- 宽度:第一个 `#100px``#10%``#auto`
- 高度:第二个 `#100px``#10%``#auto`
- 左对齐:`#left`
- 右对齐:`#right`
- 居中对齐:`#center`
- 悬浮左对齐:`#float-left`
- 悬浮右对齐:`#float-right`
- 边框:`#border` 或 `#B`
- 阴影:`#shadow` 或 `#S`
- 圆角:`#radius` 或 `#R`
---
# 高阶语法手册
---
## 目录
**说明**
- 使用`[[toc]]`,会自动生成一个页面目录,目录内容由一级、二级、三级标题组成
---
## 信息面板
**说明**
使用连续三个冒号`:::`和关键字(`[primary | info | warning | danger | success]`)来声明
```
:::primary // [primary | info | warning | danger | success] 标题
内容
:::
```
**效果**
:::p 标题
内容
:::
:::success
内容
:::
---
## 手风琴
**说明**
使用连续三个加号`+++`和关键字(`[ + | - ]`)来声明,关键字`+`表示默认收起,关键字`-`表示默认展开
```
+++ 点击展开更多
内容
++- 默认展开
内容
++ 默认收起
内容
+++
```
**效果**
+++ 点击展开更多
内容
++- 默认展开
内容
++ 默认收起
内容
+++
---
## 语法高亮
**说明**
- 在```后面指明语法名
- 加强的代码块,支持四十一种编程语言的语法高亮的显示
**效果**
非代码示例:
```
$ sudo apt-get install vim-gnome
```
Python 示例:
```python
@requires_authorization
def somefunc(param1='', param2=0):
'''A docstring'''
if param1 > param2: # interesting
print 'Greater'
return (param2 - param1 + 1) or None
class SomeClass:
pass
>>> message = '''interpreter
... prompt'''
```
JavaScript 示例:
```javascript
/**
* nth element in the fibonacci series.
* @param n >= 0
* @return the nth element, >= 0.
*/
function fib(n) {
var a = 1,
b = 1;
var tmp;
while (--n >= 0) {
tmp = a;
a += b;
b = tmp;
}
return a;
}
document.write(fib(10));
```
---
## checklist[^不通用提醒]
**说明**
- 输入`[ ]`或`[x]`,就会生成一个 checklist
**示例**
```
- [ ] AAA
- [x] BBB
- [ ] CCC
```
**效果**
- [ ] AAA
- [x] BBB
- [ ] CCC
---
## 公式[^不通用提醒]
**说明**
- 输入`$$`或`$`,就会生成一个公式
- 访问 [MathJax](http://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference) 参考更多使用方法
**示例**
```
块级公式:$$
\begin{aligned}
P(B|A)&=\frac{P(AB)}{P(A)}\\
P(\overline{B}|A)&=1-P(B|A)=1-\frac{P(AB)}{P(A)}
\end{aligned}
$$
行内公式: $e=mc^2$
```
**效果**
块级公式:$$
\begin{aligned}
P(B|A)&=\frac{P(AB)}{P(A)}\\
P(\overline{B}|A)&=1-P(B|A)=1-\frac{P(AB)}{P(A)}
\end{aligned}
$$
行内公式: $e=mc^2$
-----
## 插入音视频
**说明**
- 使用 `!v[描述](视频链接地址)` 插入视频
- 使用 `!v[描述](视频链接地址){poster=封面地址}` 插入视频并配上封面
- 使用 `!audio[描述](视频链接地址)` 插入音频
**示例**
```
这是个演示视频 !video[不带封面演示视频](/cherry/images/demo.mp4)
这是个演示视频 !video[带封面演示视频](/cherry/images/demo.mp4){poster=images/demo-dog.png}
这是个假音频!audio[描述](视频链接地址)
```
**效果**
这是个演示视频 !video[不带封面演示视频](/cherry/images/demo.mp4)
这是个演示视频 !video[带封面演示视频](/cherry/images/demo.mp4){poster=images/demo-dog.png}
这是个假音频!audio[描述](视频链接地址)
-----
## 带对齐功能的表格
**说明**
- 一种比较通用的markdown表格语法
**示例**
```
|项目(居中对齐)|价格(右对齐)|数量(左对齐)|
|:-:|-:|:-|
|计算机|¥1600|5|
|手机机|¥12|50|
```
**效果**
|项目(居中对齐)|价格(右对齐)|数量(左对齐)|
|:-:|-:|:-|
|计算机|¥1600|5|
|手机机|¥12|50|
-----
## 流程图[^不通用提醒]
**说明**
- 访问[Mermaid 流程图](https://mermaid-js.github.io/mermaid/#/flowchart)参考具体使用方法。
**效果**
小明老婆让小明下班时买一斤包子,如果遇到卖西瓜的,买一个。
左右结构
```mermaid
graph LR
A[公司] -->| 下 班 | B(菜市场)
B --> C{看见<br>卖西瓜的}
C -->|Yes| D[买一个包子]
C -->|No| E[买一斤包子]
```
上下结构
```mermaid
graph TD
A[公司] -->| 下 班 | B(菜市场)
B --> C{看见<br>卖西瓜的}
C -->|Yes| D[买一个包子]
C -->|No| E[买一斤包子]
```
-----
## 时序图[^不通用提醒]
**说明**
- 访问[Mermaid 时序图](https://mermaid-js.github.io/mermaid/#/sequenceDiagram)参考具体使用方法
**效果**
```mermaid
sequenceDiagram
A-->A: 文本1
A->>B: 文本2
loop 循环1
loop 循环2
A->B: 文本3
end
loop 循环3
B -->>A: 文本4
end
B -->> B: 文本5
end
```
-----
## 状态图[^不通用提醒]
**说明**
- 访问[Mermaid 状态图](https://mermaid-js.github.io/mermaid/#/stateDiagram)参考具体使用方法
**效果**
```mermaid
stateDiagram
[*] --> A
A --> B
A --> C
state A {
[*] --> D
D --> [*]
}
B --> [*]
C --> [*]
```
-----
## UML图[^不通用提醒]
**说明**
- 访问[Mermaid UML图](https://mermaid-js.github.io/mermaid/#/classDiagram)参考具体使用方法
**效果**
```mermaid
classDiagram
Base <|-- One
Base <|-- Two
Base : +String name
Base: +getName()
Base: +setName(String name)
class One{
+String newName
+getNewName()
}
class Two{
-int id
-getId()
}
```
-----
## 饼图[^不通用提醒]
**说明**
- 访问[Mermaid 饼图](https://mermaid-js.github.io/mermaid/#/pie)参考具体使用方法
**效果**
```mermaid
pie
title 饼图
"A" : 40
"B" : 30
"C" : 20
"D" : 10
```
-----
## 注释[^不通用提醒]
**说明**
- 使用中括号+冒号([]:)生成单行注释
- 使用中括号+尖号+冒号([^]:)生成多行注释
- 多行注释以连续两次回车结束
**示例**
```
下面是一行单行注释
[注释摘要]: 这是一段注释,不会显示到页面上
上面面是一行单行注释
下面是多行注释
[^注释摘要]: 这是一段多行注释,不会显示到页面上
可以换行
可以缩进
以两次回车结束
上面是多行注释
```
**效果**
下面是一行单行注释
[注释摘要]: 这是一段注释,不会显示到页面上
上面面是一行单行注释
下面是多行注释
[^注释摘要]: 这是一段多行注释,不会显示到页面上
可以换行
可以缩进
以两次回车结束
上面是多行注释
-----
## 脚注[^不通用提醒]
**说明**
- 在段落中引用多行注释即会生成脚注
- 脚注中括号中的数字以引用脚注的顺序自动生成
- 点击脚注的数字可以跳转到脚注详情或回到引用脚注位置
**示例**
```
这里需要一个脚注[^脚注别名1],另外这里也需要一个脚注[^another]。
[^脚注别名1]: 无论脚注内容写在哪里,脚注的内容总会显示在页面最底部
以两次回车结束
[^another]: 另外脚注里也可以使用一些简单的markdown语法
>比如 !!#ff0000 这里!!有一段**引用**
```
**效果**
这里需要一个脚注[^脚注别名1],另外这里也需要一个脚注[^another]。
[^脚注别名1]: 无论脚注内容写在哪里,脚注的内容总会显示在页面最底部
以两次回车结束
[^another]: 另外脚注里也可以使用一些简单的markdown语法
>比如 !!#ff0000 这里!!有一段**引用**
-----
# 编辑器操作能力
-----
## 通过快捷按钮修改字体样式
![bubble menu](/cherry/images/feature_font.png)
-----
## 复制html内容粘贴成markdown
**说明**
- 粘贴html内容时会自动转成markdown也可以选择粘贴为纯文本格式
- 可以拖拽调整预览区域的宽度
![copy and paste](/cherry/images/feature_copy.gif)
-----
## 快捷键
| 功能| 按键|
|--|--|
|1级标题| `Ctrl + 1`|
|2级标题| `Ctrl + 2`|
|3级标题| `Ctrl + 3`|
|4级标题| `Ctrl + 4`|
|5级标题| `Ctrl + 5`|
|6级标题| `Ctrl + 6`|
|加粗| `Ctrl + b`|
|斜体| `Ctrl + i` |
|插入链接| `Ctrl + l` |
|插入代码块| `Ctrl + k` |
|插入图片| `Ctrl + g` |
|插入公式| `Ctrl + m` |
## 协议
```
/**
* Tencent is pleased to support the open source community by making CherryMarkdown available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
* The below software in this distribution may have been modified by THL A29 Limited ("Tencent Modifications").
*
* All Tencent Modifications are Copyright (C) THL A29 Limited.
*
* CherryMarkdown is licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
```
[^专有语法提醒]: 该语法是**CherryMarkdown专有语法**可能无法在其他markdown平台上使用该语法
[^不通用提醒]: 该语法不是markdown通用语法无法保证在其他markdown平台上进行正确渲染
# 特性展示
## 语法特性
> 支持了所有常用的、通用的语法,除此之外我们还支持了一些有意思的语法
### 特性 1图片缩放、对齐、引用
#### 语法
`![img #宽度#高度#对齐方式][图片URL或引用]`
> 其中,`宽度`、`高度`支持绝对像素值比如200px、相对外层容器百分比比如50%
`对齐方式`候选值有左对齐缺省、右对齐right、居中center、悬浮左、右对齐float-left/right
![图片尺寸](/cherry/images/feature_image_size.png)
-----
### 特性 2根据表格内容生成图表
![表格图表](/cherry/images/feature_table_chart.png)
-----
### 特性 3字体颜色、字体大小
![字体样式](/cherry/images/feature_font.png)
------
## 功能特性
### 特性 1复制Html粘贴成MD语法
![html转md](/cherry/images/feature_copy.gif)
#### 使用场景
- Markdown初学者快速熟悉MD语法的一个途径
- 为调用方提供一个历史富文本数据迁成Markdown数据的方法
----
### 特性 2经典换行&常规换行
![br](/cherry/images/feature_br.gif)
#### 使用场景
团队对markdown源码有最大宽度限制一键切回经典换行两个及以上连续换行才算一个换行
-----
### 特性 3: 多光标编辑
![br](/cherry/images/feature_cursor.gif)
#### 使用场景
想要批量修改?可以试试多光标编辑(快捷键、搜索多光标选中等功能正在开发中)
### 特性 4图片尺寸
![wysiwyg](/cherry/images/feature_image_wysiwyg.gif)
### 特性 5导出
![wysiwyg](/cherry/images/feature_export.png)
-------
## 性能特性
### 局部渲染
> CherryMarkdown会判断用户到底变更了哪个段落做到只渲染变更的段落从而提升修改时的渲染性能
![wysiwyg](/cherry/images/feature_myers.png)
### 局部更新
> CherryMarkdown利用virtual dom机制实现对预览区域需要变更的内容进行局部更新的功能从而减少了浏览器Dom操作提高了修改时预览内容更新的性能
![wysiwyg](/cherry/images/feature_vdom.gif)

File diff suppressed because one or more lines are too long

@ -13,12 +13,12 @@ interface item {
subs?: item[] subs?: item[]
} }
const default_menu = [ const default_menu: item[] = [
{ ico: 'home', name: 'menu.app', path: '/' }, // { ico: 'home', name: 'menu.app', path: '/' },
{ ico: 'user', name: 'menu.user', path: '/user' }, // { ico: 'user', name: 'menu.user', path: '/user' },
{ ico: 'file-exception', name: 'menu.doc', path: '/docs' }, // { ico: 'file-exception', name: 'menu.doc', path: '/docs' },
{ ico: 'data-view', name: 'menu.appstat', path: '/stats' }, // { ico: 'data-view', name: 'menu.appstat', path: '/stats' },
{ ico: 'setting', name: 'menu.setting', path: '/setting' }, // { ico: 'setting', name: 'menu.setting', path: '/setting' },
] ]
export const useMenuStore = defineStore('menu', { export const useMenuStore = defineStore('menu', {

Loading…
Cancel
Save