master
veypi 2 years ago
parent 8f2bcc1591
commit 7990fbcf10

65
oab/Cargo.lock generated

@ -19,6 +19,29 @@ dependencies = [
"tokio-util",
]
[[package]]
name = "actix-files"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d832782fac6ca7369a70c9ee9a20554623c5e51c76e190ad151780ebea1cf689"
dependencies = [
"actix-http",
"actix-service",
"actix-utils",
"actix-web",
"askama_escape",
"bitflags",
"bytes",
"derive_more",
"futures-core",
"http-range",
"log",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite",
]
[[package]]
name = "actix-http"
version = "3.2.1"
@ -266,6 +289,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "askama_escape"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "atoi"
version = "0.4.0"
@ -819,6 +848,12 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-range"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]]
name = "httparse"
version = "1.7.1"
@ -1024,6 +1059,16 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -1154,6 +1199,7 @@ dependencies = [
name = "oab"
version = "0.1.0"
dependencies = [
"actix-files",
"actix-web",
"aes-gcm",
"base64",
@ -1165,6 +1211,7 @@ dependencies = [
"include_dir",
"jsonwebtoken",
"lazy_static",
"proc",
"rand",
"serde",
"serde-big-array",
@ -1327,6 +1374,15 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "proc"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@ -2038,6 +2094,15 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.8"

@ -22,6 +22,7 @@ thiserror = "1.0"
sqlx = { version = "0.5", features = [ "runtime-tokio-rustls", "mysql", "macros", "migrate", "chrono"] }
actix-web = "4"
actix-files = "0.6.2"
jsonwebtoken = "8"
aes-gcm="0.9"
@ -33,3 +34,5 @@ base64 = "0.13.0"
uuid = { version = "1.1", features = ["v3","v4", "fast-rng", "macro-diagnostics"]}
serde_repr = "0.1.8"
proc = {path="proc"}

@ -137,13 +137,17 @@ INSERT INTO `app` (`id`, `name`, `key`, `role_id`)
VALUES ('FR9P5t8debxc11aFF', 'oa', 'AMpjwQHwVjGsb1WC4WG6', '1lytMwQL4uiNd0vsc');
INSERT INTO `resource` (`app_id`, `name`)
VALUES ('FR9P5t8debxc11aFF', 'app');
VALUES ('FR9P5t8debxc11aFF', 'app'),
('FR9P5t8debxc11aFF', 'user');
INSERT INTO `role` (`id`, `app_id`, `name`)
VALUES ('1lytMwQL4uiNd0vsc', 'FR9P5t8debxc11aFF', 'admin');
INSERT INTO `access` (`app_id`, `name`, `role_id`, `user_id`)
VALUES ('FR9P5t8debxc11aFF', 'app', '1lytMwQL4uiNd0vsc', null);
INSERT INTO `access` (`app_id`, `name`, `role_id`, `user_id`,`level`)
VALUES ('FR9P5t8debxc11aFF', 'app', '1lytMwQL4uiNd0vsc', NULL,6),
('FR9P5t8debxc11aFF', 'user', '1lytMwQL4uiNd0vsc', NULL,6);
ALTER TABLE `app`
ADD FOREIGN KEY (`role_id`) REFERENCES `role`(`id`);

47
oab/proc/Cargo.lock generated

@ -0,0 +1,47 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "proc"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"

@ -0,0 +1,14 @@
[package]
name = "proc"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
quote = "1"
proc-macro2 = "1.0"
syn = { version = "1.0", features = ["full", "derive", "extra-traits"] }

@ -0,0 +1,158 @@
//
// access.rs
// Copyright (C) 2022 veypi <i@veypi.com>
// 2022-09-16 00:13
// Distributed under terms of the Apache license.
//
use proc_macro2::{Ident, Span};
use quote::{quote, ToTokens};
use syn::{AttributeArgs, ItemFn, NestedMeta, ReturnType};
pub struct AccessWrap {
cb_fn: Option<Ident>,
func: ItemFn,
access: Access,
}
impl AccessWrap {
pub fn new(args: AttributeArgs, func: ItemFn, cb_fn: Option<&str>) -> syn::Result<Self> {
let cb_fn: Option<Ident> = match cb_fn {
Some(cb) => Some(syn::parse_str(cb)?),
None => None,
};
let args = Access::new(args)?;
Ok(Self {
cb_fn,
func,
access: args,
})
}
}
impl ToTokens for AccessWrap {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let func_vis = &self.func.vis;
let func_block = &self.func.block;
let fn_sig = &self.func.sig;
let fn_attrs = &self.func.attrs;
let fn_name = &fn_sig.ident;
let fn_generics = &fn_sig.generics;
let fn_args = &fn_sig.inputs;
let fn_async = &fn_sig.asyncness.unwrap();
let fn_output = match &fn_sig.output {
ReturnType::Type(ref _arrow, ref ty) => ty.to_token_stream(),
ReturnType::Default => {
quote! {()}
}
};
let resp = quote!(Err(Error::NotAuthed));
let permissions = &self.access.domain;
// let args = quote! {
// #(#permissions,)*
// };
let stream = match &self.cb_fn {
Some(cb_fn) => {
let condition = match &self.access.did {
Some(did) => {
quote! {
let _auth_did = #did;
if _auth_token.#cb_fn(#permissions, _auth_did)
}
}
None => {
quote! {
if _auth_token.#cb_fn(#permissions, "")
}
}
};
quote! {
#(#fn_attrs)*
#func_vis #fn_async fn #fn_name #fn_generics(
_auth_token: Option<actix_web::web::ReqData<crate::models::Token>>,
#fn_args
) -> #fn_output {
let _auth_token = match _auth_token {
Some(_auth_token) => _auth_token.into_inner(),
None => {
return #resp
}
};
#condition {
let f = || async move #func_block;
f().await
} else {
#resp
}
}
}
}
None => {
quote! {
#(#fn_attrs)*
#func_vis #fn_async fn #fn_name #fn_generics(
_auth_token: Option<actix_web::web::ReqData<crate::models::Token>>,
#fn_args
) -> #fn_output {
if _auth_token.is_some() {
let f = || async move #func_block;
f().await
} else {
#resp
}
}
}
}
};
let _stream = tokens.extend(stream);
}
}
struct Access {
domain: syn::LitStr,
did: Option<syn::Expr>,
}
impl Access {
fn new(args: AttributeArgs) -> syn::Result<Self> {
let mut domain: Option<syn::LitStr> = None;
let mut did = None;
for arg in args {
match arg {
NestedMeta::Lit(syn::Lit::Str(lit)) => {
domain = Some(lit);
}
NestedMeta::Meta(syn::Meta::NameValue(syn::MetaNameValue {
path,
lit: syn::Lit::Str(lit_str),
..
})) => {
if path.is_ident("id") {
let expr = lit_str.parse().unwrap();
did = Some(expr);
} else {
return Err(syn::Error::new_spanned(
path,
"Unknown identifier. Available: 'id'",
));
}
}
_ => {
return Err(syn::Error::new_spanned(arg, "Unknown attribute."));
}
}
}
match domain {
Some(domain) => Ok(Self { domain, did }),
None => Err(syn::Error::new(
Span::call_site(),
"The #[access(..)] macro requires one `auth` argument",
)),
}
}
}

@ -0,0 +1,131 @@
//
// lib.rs
// Copyright (C) 2022 veypi <i@veypi.com>
// 2022-09-16 00:07
// Distributed under terms of the Apache license.
//
//
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, AttributeArgs, ItemFn};
mod access;
use access::AccessWrap;
#[proc_macro_attribute]
pub fn have_access(args: TokenStream, input: TokenStream) -> TokenStream {
check_permissions(None, args, input)
}
#[proc_macro_attribute]
pub fn access_read(args: TokenStream, input: TokenStream) -> TokenStream {
check_permissions(Some("can_read"), args, input)
}
#[proc_macro_attribute]
pub fn access_create(args: TokenStream, input: TokenStream) -> TokenStream {
check_permissions(Some("can_create"), args, input)
}
#[proc_macro_attribute]
pub fn access_update(args: TokenStream, input: TokenStream) -> TokenStream {
check_permissions(Some("can_update"), args, input)
}
#[proc_macro_attribute]
pub fn access_delete(args: TokenStream, input: TokenStream) -> TokenStream {
check_permissions(Some("can_delete"), args, input)
}
fn check_permissions(cb_fn: Option<&str>, args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as AttributeArgs);
let func = parse_macro_input!(input as ItemFn);
match AccessWrap::new(args, func, cb_fn) {
Ok(ac) => ac.into_token_stream().into(),
Err(err) => err.to_compile_error().into(),
}
}
#[proc_macro_derive(MyDisplay)]
#[doc(hidden)]
pub fn display(input: TokenStream) -> TokenStream {
// Parse the string representation
let ast: syn::DeriveInput = syn::parse(input).unwrap();
match ast.data {
syn::Data::Enum(ref enum_data) => {
let name = &ast.ident;
impl_display(name, enum_data).into()
}
_ => panic!("#[derive(Display)] works only on enums"),
}
}
fn impl_display(name: &syn::Ident, data: &syn::DataEnum) -> proc_macro2::TokenStream {
let variants = data
.variants
.iter()
.map(|variant| impl_display_for_variant(name, variant));
quote! {
impl ::std::fmt::Display for #name {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> {
match *self {
#(#variants)*
}
}
}
}
}
fn impl_display_for_variant(name: &syn::Ident, variant: &syn::Variant) -> proc_macro2::TokenStream {
let id = &variant.ident;
match variant.fields {
syn::Fields::Unit => match &variant.discriminant {
// print true value of enummember
// enum {
// a = 1
//
// }
Some((_, value)) => {
quote! {
#name::#id => {
f.write_str(stringify!(#value).to_lowercase().as_str())
}
}
}
_ => {
// print lowercase name of enummember
quote! {
#name::#id => {
f.write_str(stringify!(#id).to_lowercase().as_str())
}
}
}
},
syn::Fields::Unnamed(ref fields) => match fields.unnamed.len() {
0 => {
quote! {
#name::#id() => {
f.write_str(stringify!(#id))?;
f.write_str("()")
}
}
}
1 => {
quote! {
#name::#id(ref inner) => {
::std::fmt::Display::fmt(inner, f)
}
}
}
_ => {
panic!(
"#[derive(Display)] does not support tuple variants with more than one \
fields"
)
}
},
_ => panic!("#[derive(Display)] works only with unit and tuple variants"),
}
}

@ -6,10 +6,14 @@
//
//
use actix_web::{delete, get, post, web, Responder};
use proc::access_read;
use serde::{Deserialize, Serialize};
use crate::{models, Error, Result, CONFIG};
use chrono::NaiveDateTime;
#[get("/app/{id}")]
#[access_read("app")]
pub async fn get(id: web::Path<String>) -> Result<impl Responder> {
let n = id.into_inner();
if !n.is_empty() {
@ -23,11 +27,35 @@ pub async fn get(id: web::Path<String>) -> Result<impl Responder> {
}
}
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct App {
pub id: String,
pub created: Option<NaiveDateTime>,
pub updated: Option<NaiveDateTime>,
pub name: Option<String>,
pub des: Option<String>,
pub icon: Option<String>,
pub user_count: i64,
pub hide: bool,
pub join_method: models::AppJoin,
pub role_id: Option<String>,
pub redirect: Option<String>,
pub status: i64,
pub u_status: i64,
}
#[get("/app/")]
#[access_read("app")]
pub async fn list() -> Result<impl Responder> {
let result = sqlx::query_as::<_, models::App>("select * from app")
let result = sqlx::query_as::<_, App>(
"select app.id,app.created, app.updated, app.icon, app.name, app.des, app.user_count, app.hide,app.join_method, app.role_id, app.redirect, app.status, app_user.status as u_status from app left join app_user on app_user.user_id = ? && app_user.app_id = app.id",
).bind(_auth_token.id)
.fetch_all(CONFIG.db())
.await?;
Ok(web::Json(result))
}

@ -10,29 +10,60 @@ use std::fmt::Debug;
use crate::{models, Error, Result, CONFIG};
use actix_web::{delete, get, head, http, post, web, HttpResponse, Responder};
use base64;
use proc::access_read;
use rand::Rng;
use serde::{Deserialize, Serialize};
use tracing::info;
#[get("/user/{id}")]
pub async fn get(id: web::Path<String>) -> Result<models::User> {
#[access_read("user", id = "&id.clone()")]
pub async fn get(id: web::Path<String>) -> Result<impl Responder> {
let n = id.into_inner();
if !n.is_empty() {
let s = sqlx::query_as::<_, models::User>("select *& from user where id = ?")
.bind(n)
.fetch_one(CONFIG.db())
.await?;
info!("{:#?}", s);
Ok(s)
let s = sqlx::query!(
"select id,updated,created,username,nickname,email,icon,status, used, space from user where id = ?",n
).map(|row| models::User {
id: row.id,
created: row.created,
updated: row.updated,
username: row.username,
nickname: row.nickname,
email: row.email,
status: row.status,
used: row.used,
space: row.space.unwrap_or(0),
icon: row.icon,
..Default::default()
})
.fetch_one(CONFIG.db())
.await?;
Ok(web::Json(s))
} else {
Err(Error::Missing("id".to_string()))
}
}
#[get("/user/")]
#[access_read("user")]
pub async fn list() -> Result<impl Responder> {
let result = sqlx::query_as::<_, models::User>("select * from user")
.fetch_all(CONFIG.db())
.await?;
let result = sqlx::query!(
"select id,updated,created,username,nickname,email,icon,status, used, space from user",
)
.map(|row| models::User {
id: row.id,
created: row.created,
updated: row.updated,
username: row.username,
nickname: row.nickname,
email: row.email,
status: row.status,
used: row.used,
space: row.space.unwrap_or(0),
icon: row.icon,
..Default::default()
})
.fetch_all(CONFIG.db())
.await?;
Ok(web::Json(result))
}
@ -126,8 +157,15 @@ values ( ?, ?, ? )
}
};
if i == 0 {
let result = sqlx::query_as::<_, models::AccessCore>(
"select access.name,access.rid,access.level from access, user_role, role WHERE user_role.user_id = ? && access.role_id=user_role.role_id && role.id=user_role.role_id && role.app_id = ?",
)
.bind(&u.id)
.bind(CONFIG.uuid.clone())
.fetch_all(CONFIG.db())
.await?;
Ok(HttpResponse::build(http::StatusCode::OK)
.insert_header(("auth_token", u.token().to_string()?))
.insert_header(("auth_token", u.token(result).to_string()?))
.body("".to_string()))
} else {
Ok(HttpResponse::build(http::StatusCode::OK)
@ -157,6 +195,7 @@ pub async fn register(q: web::Json<RegisterOpt>) -> Result<String> {
None => {
let mut u = models::User::default();
u.username = q.username.clone();
u.id = uuid::Uuid::new_v4().to_string().replace("-", "");
let p = match base64::decode(q.password.as_bytes()) {
Err(_) => return Err(Error::ArgInvalid("password".to_string())),
Ok(p) => p,
@ -167,6 +206,9 @@ pub async fn register(q: web::Json<RegisterOpt>) -> Result<String> {
};
info!("{}", p);
u.update_pass(&p)?;
let mut rng = rand::thread_rng();
let idx: i64 = rng.gen_range(1..221);
u.icon = Some(format!("/media/icon/usr/{:04}.jpg", idx));
u
}
};
@ -180,22 +222,28 @@ pub async fn register(q: web::Json<RegisterOpt>) -> Result<String> {
au.user_id = u.id.clone();
match oa.join_method {
models::app::AppJoin::Disabled => return Err(Error::AppDisabledRegister),
models::app::AppJoin::Auto => au.status = models::app::AUStatus::OK,
models::app::AppJoin::Auto => {
au.status = models::app::AUStatus::OK;
}
models::app::AppJoin::Applying => au.status = models::app::AUStatus::Applying,
}
let mut c = CONFIG.db().begin().await?;
// 创建用户
sqlx::query!(
r#"
insert into user (id,username,real_code,check_code)
values ( ?, ?, ?, ?)
insert into user (id,username,real_code,check_code,icon)
values ( ?, ?, ?, ?, ?)
"#,
u.id,
u.username,
u.real_code,
u.check_code,
u.icon,
)
.execute(&mut c)
.await?;
// 关联应用
sqlx::query!(
r#"
insert into app_user ( app_id, user_id, status)
@ -207,6 +255,23 @@ values ( ?, ?, ?, ?)
)
.execute(&mut c)
.await?;
if oa.role_id.is_some() {
match au.status {
models::app::AUStatus::OK => {
sqlx::query!(
r#"
insert into user_role (user_id, role_id)
values (?, ?)
"#,
au.user_id,
oa.role_id.unwrap(),
)
.execute(&mut c)
.await?;
}
_ => {}
}
}
c.commit().await?;
Ok("ok".to_string())
}

@ -62,6 +62,7 @@ pub struct ApplicationConfig {
pub key: String,
pub debug: bool,
pub server_url: String,
pub media_path: String,
pub db_url: String,
pub db_user: String,
pub db_pass: String,
@ -108,6 +109,7 @@ impl ApplicationConfig {
key: "AMpjwQHwVjGsb1WC4WG6".to_string(),
debug: true,
server_url: "127.0.0.1:4001".to_string(),
media_path: "/Users/veypi/test/media/".to_string(),
db_url: "127.0.0.1:3306".to_string(),
db_user: "root".to_string(),
db_pass: "123456".to_string(),

@ -0,0 +1,80 @@
//
// auth.rs
// Copyright (C) 2022 veypi <i@veypi.com>
// 2022-09-01 17:39
// Distributed under terms of the Apache license.
//
use std::cell::RefCell;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
use actix_web::body::MessageBody;
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::http::header::HeaderValue;
use actix_web::{Error, HttpMessage};
use futures_util::future::{ok, Ready};
use futures_util::Future;
use tracing::warn;
use crate::models;
// custom request auth middleware
pub struct Auth;
impl<S, B> Transform<S, ServiceRequest> for Auth
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = AuthMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(AuthMiddleware {
service: Rc::new(RefCell::new(service)),
})
}
}
pub struct AuthMiddleware<S> {
service: Rc<RefCell<S>>,
}
impl<S, B> Service<ServiceRequest> for AuthMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let svc = self.service.clone();
Box::pin(async move {
let value = HeaderValue::from_str("").unwrap();
let token = req.headers().get("auth_token").unwrap_or(&value);
let token = models::Token::from(token.to_str().unwrap_or(""));
match token {
Ok(t) => {
req.extensions_mut().insert(t.id.clone());
req.extensions_mut().insert(t);
}
Err(e) => warn!("{}", e),
};
Ok(svc.call(req).await?)
})
}
}

@ -5,6 +5,8 @@
// Distributed under terms of the Apache license.
//
pub mod auth;
use std::future::{ready, Ready};
use actix_web::{

@ -4,14 +4,17 @@
// 2022-07-07 23:51
// Distributed under terms of the Apache license.
//
use actix_files as fs;
use actix_web::{
middleware,
dev,
http::StatusCode,
middleware::{self, ErrorHandlerResponse, ErrorHandlers},
web::{self, Data},
App, HttpServer,
};
use oab::{api, init_log, models, Clis, Result, CLI, CONFIG};
use tracing::{info, warn};
use oab::{api, init_log, libs, models, Clis, Result, CLI, CONFIG};
use tracing::{error, info, warn};
#[tokio::main]
async fn main() -> Result<()> {
@ -48,8 +51,14 @@ async fn web() -> Result<()> {
let app = App::new();
app.wrap(logger)
.wrap(middleware::Compress::default())
.service(fs::Files::new("/media", CONFIG.media_path.clone()).show_files_listing())
.service(
web::scope("api")
.wrap(
ErrorHandlers::new()
.handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header),
)
.wrap(libs::auth::Auth)
.app_data(json_config)
.app_data(Data::new(CONFIG.db()))
.configure(api::routes),
@ -59,3 +68,11 @@ async fn web() -> Result<()> {
serv.bind(CONFIG.server_url.clone())?.run().await?;
Ok(())
}
fn add_error_header<B>(
res: dev::ServiceResponse<B>,
) -> std::result::Result<ErrorHandlerResponse<B>, actix_web::Error> {
error!("{}", res.response().error().unwrap());
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
}

@ -11,9 +11,9 @@ mod user;
use tracing::info;
pub use app::{AUStatus, App, AppUser};
pub use app::{AUStatus, App, AppJoin, AppUser};
pub use role::{Access, Resource, Role};
pub use user::User;
pub use user::{AccessCore, AccessLevel, Token, User};
use crate::CONFIG;

@ -20,8 +20,10 @@ use block_padding::{Padding, Pkcs7};
use generic_array::typenum::U32;
use generic_array::GenericArray;
use proc::MyDisplay;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use serde_repr::*;
fn rand_str(l: usize) -> String {
thread_rng()
@ -43,7 +45,7 @@ fn rand_str(l: usize) -> String {
// block
// }
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
#[derive(Debug, Default, Serialize, Deserialize, sqlx::FromRow)]
pub struct User {
pub id: String,
pub created: Option<NaiveDateTime>,
@ -63,13 +65,21 @@ pub struct User {
}
impl User {
pub fn token(&self) -> Token {
pub fn token(&self, ac: Vec<AccessCore>) -> Token {
let default_ico = "/media/".to_string();
let t = Token {
iss: "oa".to_string(),
iss: "onedt".to_string(),
aud: "".to_string(),
exp: (Utc::now() + Duration::days(4)).timestamp(),
iat: Utc::now().timestamp(),
id: self.id.clone(),
ico: self.icon.as_ref().unwrap_or(&default_ico).to_string(),
access: Some(ac),
nickname: self
.nickname
.as_ref()
.unwrap_or(&self.username.clone())
.to_string(),
};
t
}
@ -126,28 +136,6 @@ impl User {
}
}
impl Default for User {
fn default() -> Self {
Self {
id: uuid::Uuid::new_v4().to_string().replace("-", ""),
created: None,
updated: None,
delete_flag: false,
username: "".to_string(),
nickname: None,
email: None,
phone: None,
icon: None,
check_code: None,
real_code: None,
status: 0,
used: 0,
space: 300,
}
}
}
impl actix_web::Responder for User {
type Body = actix_web::body::BoxBody;
@ -164,13 +152,55 @@ impl actix_web::Responder for User {
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)]
pub struct Access {
pub created: Option<NaiveDateTime>,
pub updated: Option<NaiveDateTime>,
pub user_id: String,
pub domain: String,
pub did: Option<String>,
pub l: AccessLevel,
}
#[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)]
pub struct AccessCore {
pub name: String,
pub rid: Option<String>,
pub level: AccessLevel,
}
#[derive(
MyDisplay,
Debug,
Deserialize_repr,
Serialize_repr,
Clone,
sqlx::Type,
PartialEq,
Eq,
PartialOrd,
Ord,
)]
#[repr(i64)]
pub enum AccessLevel {
No = 0,
Read = 1,
Create = 2,
Update = 3,
Delete = 4,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Token {
pub iss: String, // Optional. token 发行者
pub aud: String, // Optional. token 使用者
pub exp: i64, // Required 失效时间
pub iat: i64, // Optional. 发布时间
pub id: String, // 用户id
pub nickname: String,
pub ico: String,
pub access: Option<Vec<AccessCore>>,
}
impl Token {
@ -180,7 +210,19 @@ impl Token {
&DecodingKey::from_secret("secret".as_ref()),
&Validation::default(),
)?;
Ok(token.claims)
if token.claims.is_valid() {
Ok(token.claims)
} else {
Err(Error::ExpiredToken)
}
}
pub fn is_valid(&self) -> bool {
info!("{}/{}", self.exp, Utc::now().timestamp());
if self.exp > Utc::now().timestamp() {
true
} else {
false
}
}
pub fn to_string(&self) -> Result<String> {
let token = encode(
@ -190,4 +232,41 @@ impl Token {
)?;
Ok(token)
}
fn check(&self, domain: &str, did: &str, l: AccessLevel) -> bool {
println!("{:#?}|{:#?}|{}|", self.access, domain, did);
match &self.access {
Some(ac) => {
for ele in ac {
if ele.name == domain && ele.level >= l {
match &ele.rid {
Some(temp) => {
if temp == did {
return true;
}
}
None => return true,
}
}
}
false
}
None => false,
}
}
pub fn can_read(&self, domain: &str, did: &str) -> bool {
self.check(domain, did, AccessLevel::Read)
}
pub fn can_create(&self, domain: &str, did: &str) -> bool {
self.check(domain, did, AccessLevel::Create)
}
pub fn can_update(&self, domain: &str, did: &str) -> bool {
self.check(domain, did, AccessLevel::Update)
}
pub fn can_delete(&self, domain: &str, did: &str) -> bool {
self.check(domain, did, AccessLevel::Delete)
}
}

@ -5,11 +5,9 @@
// Distributed under terms of the Apache license.
//
use actix_web::http::header;
use actix_web::middleware::ErrorHandlerResponse;
use actix_web::ResponseError;
use actix_web::{
dev, error,
error,
http::{header::ContentType, StatusCode},
HttpResponse,
};
@ -19,6 +17,28 @@ use thiserror::Error as ThisError;
use tracing::info;
pub type Result<T> = std::result::Result<T, Error>;
pub type JsonResult<T> = std::result::Result<JsonResponse<T>, Error>;
#[derive(Serialize, Deserialize)]
pub struct JsonResponse<T> {
pub content: T,
}
impl<T> From<T> for JsonResponse<T> {
fn from(e: T) -> Self {
Self { content: e }
}
}
impl<T> actix_web::Responder for JsonResponse<T>
where
T: serde::Serialize,
{
type Body = actix_web::body::BoxBody;
fn respond_to(self, _req: &actix_web::HttpRequest) -> HttpResponse<Self::Body> {
HttpResponse::build(StatusCode::OK)
.insert_header(ContentType::json())
.body(serde_json::to_string(&self.content).unwrap())
}
}
// pub type AsyncResult<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
@ -65,6 +85,11 @@ pub enum Error {
InvalidSessionId,
#[error("invalid verify code")]
InvalidVerifyCode,
#[error("invalid token")]
InvalidToken,
#[error("expired token")]
ExpiredToken,
#[error("no access")]
NotAuthed,
#[error("login failed")]
@ -98,6 +123,7 @@ pub enum Error {
#[error("{0}")]
BusinessException(String),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader { expected: String, found: String },

@ -1,7 +1,7 @@
import {createRouter, createWebHistory, RouteLocationNormalized} from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
import util from '@/libs/util'
import {Auths, R} from '@/auth'
import {store} from '@/store'
import { Auths, R } from '@/auth'
import { store } from '@/store'
declare module 'vue-router' {
@ -29,7 +29,7 @@ const router = createRouter({
{
path: '/app/:uuid?',
component: () => import('@/views/app.vue'),
redirect: {name: 'app.main'},
redirect: { name: 'app.main' },
children: [
{
path: 'main',
@ -125,7 +125,7 @@ router.beforeEach((to, from) => {
return {
name: 'login',
// 保存我们所在的位置,以便以后再来
query: {redirect: to.fullPath},
query: { redirect: to.fullPath },
}
}
if (to.meta.checkAuth) {

@ -10,9 +10,26 @@
"dependencies": {
"@veypi/one-icon": "2",
"animate.css": "^4.1.1",
"axios": "^1.2.1",
"base-64": "^1.0.0",
"fast-xml-parser": "^3.19.0",
"he": "^1.2.0",
"hot-patcher": "^0.5.0",
"js-base64": "^3.7.3",
"layerr": "^0.1.2",
"less": "^4.1.3",
"md5": "^2.3.0",
"minimatch": "^5.1.2",
"mitt": "^3.0.0",
"nested-property": "^4.0.0",
"path-posix": "^1.0.0",
"pinia": "^2.0.13",
"seamless-scroll-polyfill": "^2.2.0",
"url-join": "^4.0.1",
"url-parse": "^1.5.3",
"validator": "^13.7.0",
"vue": "^3.2.25",
"vue-i18n": "^9.2.2",
"vue-router": "4"
},
"devDependencies": {

@ -1,6 +1,22 @@
<style>
#app {
@apply h-full w-full flex justify-center items-center;
color: var(--base-color);
background: var(--base-bg-3);
}
.animate__400ms {
--animate-duration: 400ms;
}
.page-h1 {
font-size: 1.5rem;
line-height: 2rem;
margin-left: 2.5rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
::-webkit-scrollbar {
display: none;
/* Chrome Safari */
}
</style>
@ -24,7 +40,6 @@ import { useUserStore } from '@/store/user'
import { onBeforeMount } from 'vue'
let user = useUserStore()
user.setUser()
onBeforeMount(() => {
let loader = document.getElementById('loader-wrapper')
if (loader && loader.parentElement) {
@ -32,5 +47,6 @@ onBeforeMount(() => {
}
// store.dispatch('fetchSelf')
// store.dispatch('user/fetchUserData')
user.fetchUserData()
})
</script>

@ -0,0 +1,82 @@
import axios from 'axios'
import msg from '@/msg'
function getQueryVariable(variable: string) {
let query = window.location.search.substring(1)
let vars = query.split('&')
for (let i = 0; i < vars.length; i++) {
let pair = vars[i].split('=')
if (pair[0] == variable) {
return pair[1]
}
}
return ''
}
function baseRequests(url: string, method: any = 'GET', query: any, data: any, success: any, fail?: Function, header?: any) {
let headers = {
auth_token: localStorage.auth_token || decodeURIComponent(getQueryVariable('token') as string),
}
if (header) {
headers = Object.assign(headers, header)
}
return axios({
url: url,
params: query,
data: data,
method: method,
headers: headers,
}).then((res: any) => {
if ('redirect_url' in res.headers) {
window.location.href = res.headers.redirect_url
return
}
if (method === 'HEAD') {
success(res.headers)
} else {
success(res.data)
}
})
.catch((e: any) => {
if (typeof fail === 'function') {
fail(e.response)
return
}
let code = e.response.status
if (code === 400) {
msg.Warn(e.response.headers.error)
return
} else if (code === 401) {
console.log(e)
// store.commit('user/logout')
return
} else if (code === 500) {
return
}
console.log(e)
})
}
const ajax = {
get(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'GET', data, {}, success, fail, header)
},
head(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'HEAD', data, {}, success, fail, header)
},
delete(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'DELETE', data, {}, success, fail, header)
},
post(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'POST', {}, data, success, fail, header)
},
put(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'PUT', {}, data, success, fail, header)
},
patch(url: '', data = {}, success = {}, fail?: Function, header?: any) {
return baseRequests(url, 'PATCH', {}, data, success, fail, header)
},
}
export default ajax

@ -0,0 +1,49 @@
/*
* @name: app
* @author: veypi <i@veypi.com>
* @date: 2021-11-17 14:44
* @descriptionap
* @update: 2021-11-17 14:44
*/
import { Interface } from './interface'
import ajax from './ajax'
import { BaseUrl } from './setting'
export default {
local: BaseUrl + 'app/',
self() {
return new Interface(ajax.get, this.local, { option: 'oa' })
},
getKey(uuid: string) {
return new Interface(ajax.get, this.local + uuid, { option: 'key' })
},
create(name: string, icon: string) {
return new Interface(ajax.post, this.local, { name, icon })
},
get(uuid: string) {
return new Interface(ajax.get, this.local + uuid)
},
list() {
return new Interface(ajax.get, this.local)
},
update(uuid: string, props: any) {
return new Interface(ajax.patch, this.local + uuid, props)
},
user(uuid: string) {
if (uuid === '') {
uuid = '_'
}
return {
local: this.local + uuid + '/user/',
list(uid: number) {
return new Interface(ajax.get, this.local + uid)
},
add(uid: number) {
return new Interface(ajax.post, this.local + uid)
},
update(uid: number, status: string) {
return new Interface(ajax.patch, this.local + uid, { status })
},
}
},
}

@ -0,0 +1,38 @@
import {Interface} from './interface'
import ajax from './ajax'
import {BaseUrl} from './setting'
export default (uuid: string) => {
return {
local: BaseUrl + 'app/' + uuid + '/auth/',
get(id: number) {
return new Interface(ajax.get, this.local + id)
},
del(id: number) {
return new Interface(ajax.delete, this.local + id)
},
update(id: number, ResourceID: number, RUID: string, Level: number) {
return new Interface(ajax.patch, this.local + id, {
ResourceID,
RUID,
Level,
})
},
create(ResourceID: number, UserID: number | null, RoleID: number | null, RUID: string, Level: number) {
return new Interface(ajax.post, this.local, {
ResourceID,
UserID,
RoleID,
RUID,
Level,
})
},
listOfUser(user_id: number) {
return new Interface(ajax.get, this.local, {uid: user_id})
},
listOfRole(id: number) {
return new Interface(ajax.get, this.local, {rid: id})
},
}
}

@ -0,0 +1,31 @@
/*
* Copyright (C) 2019 light <veypi@light-laptop>
*
* Distributed under terms of the MIT license.
*/
import {App} from 'vue'
import role from "./role";
import app from './app'
import user from './user'
import auth from './auth'
import resource from './resource'
import token from './token'
const api = {
user: user,
token: token,
app: app,
auth: auth,
role: role,
resource: resource
}
const Api = {
install(vue: App): void {
vue.config.globalProperties.$api = api
}
}
export {Api}
export default api

@ -0,0 +1,26 @@
export type SuccessFunction<T> = (e: any) => void;
export type FailedFunction<T> = (e: any) => void;
const Code = {
42011: '无操作权限',
22031: '资源不存在 或 您无权操作该资源'
}
export class Interface {
private readonly method: Function
private readonly api: string
private readonly data: any
private readonly header: any
constructor(method: Function, api: string, data?: any, headers?: any) {
this.method = method
this.api = api
this.data = data
this.header = headers
}
Start(success?: SuccessFunction<any>, fail?: FailedFunction<any>) {
this.method(this.api, this.data, success, fail, this.header)
}
}

@ -0,0 +1,28 @@
/*
* @name: resource
* @author: veypi <i@veypi.com>
* @date: 2021-11-18 15:52
* @descriptionresource
*/
import {Interface} from './interface'
import ajax from './ajax'
import {BaseUrl} from './setting'
export default (uuid: string) => {
return {
local: BaseUrl + 'app/' + uuid + '/resource/',
list() {
return new Interface(ajax.get, this.local)
},
update(id: number, props: {}) {
return new Interface(ajax.patch, this.local + id, props)
},
create(Name: string, Des: string) {
return new Interface(ajax.post, this.local, {Name, Des})
},
delete(id: number) {
return new Interface(ajax.delete, this.local + id)
},
}
}

@ -0,0 +1,40 @@
import {BaseUrl} from './setting'
import {Interface} from './interface'
import ajax from './ajax'
export default (uuid: string) => {
return {
local: BaseUrl + 'app/' + uuid + '/role/',
get(id: number) {
return new Interface(ajax.get, this.local + id)
},
list() {
return new Interface(ajax.get, this.local)
},
update(id: number, props) {
return new Interface(ajax.patch, this.local + id, props)
},
create(Name: string, Tag: string) {
return new Interface(ajax.post, this.local, {
Name, Tag,
})
},
delete(id: number) {
return new Interface(ajax.delete, this.local + id)
},
user(id: number) {
return {
local: this.local + id + '/user/',
list() {
return new Interface(ajax.get, this.local)
},
create(uid: number) {
return new Interface(ajax.post, this.local + uid)
},
delete(uid: number) {
return new Interface(ajax.delete, this.local + uid)
},
}
},
}
}

@ -0,0 +1,10 @@
/*
* @name: setting
* @author: veypi <i@veypi.com>
* @date: 2021-11-17 15:45
* @descriptionsetting
* @update: 2021-11-17 15:45
*/
export const BaseUrl = '/api/'

@ -0,0 +1,18 @@
/*
* @name: token
* @author: veypi <i@veypi.com>
* @date: 2021-11-26 19:22
* @descriptiontoken
*/
import {Interface} from '@/api/interface'
import ajax from './ajax'
export default (uuid: string) => {
return {
local: '/api/app/' + uuid + '/token/',
get() {
return new Interface(ajax.get, this.local)
},
}
}

@ -0,0 +1,33 @@
import { Base64 } from 'js-base64'
import { Interface } from './interface'
import ajax from './ajax'
import { BaseUrl } from './setting'
export default {
local: BaseUrl + 'user/',
register(username: string, password: string, prop?: any) {
const data = Object.assign({
username: username,
password: Base64.encode(password),
}, prop)
return new Interface(ajax.post, this.local, data)
},
login(username: string, password: string) {
return new Interface(ajax.head, this.local + username, {
typ: 'username',
password: Base64.encode(password),
})
},
search(q: string) {
return new Interface(ajax.get, this.local, { username: q })
},
get(id: number) {
return new Interface(ajax.get, this.local + id)
},
list() {
return new Interface(ajax.get, this.local)
},
update(id: number, props: any) {
return new Interface(ajax.patch, this.local + id, props)
},
}

@ -0,0 +1,18 @@
/*
* dark.css
* Copyright (C) 2022 veypi
*
* Distributed under terms of the Apache license.
*/
:root[theme="dark"] {
--base-color: #fff;
--base-bg: #252525;
--base-bg-1: #303030;
--base-bg-2: #404040;
--base-bg-3: #505050;
--header-bg: #404040;
--L0: #63e2b7;
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,31 @@
/*
* light.css
* Copyright (C) 2022 veypi
*
* Distributed under terms of the Apache license.
*/
:root {
transition: all 0.2s linear;
--base-color: #000;
--base-bg: #f5f5f5;
--base-bg-1: #e0e0e0;
--base-bg-2: #d0d0d0;
--base-bg-3: #c0c0c0;
--header-bg: #d0d0d0;
--L0: #18a058;
--color-primary: #2196f3;
--color-secondary: #03a9f4;
--color-accent: #ff9800;
--color-error: #f44336;
--color-warning: #ff5722;
--color-info: #ffc107;
--color-success: #4caf50;
--input-line-default: #002f55;
--input-line-shine: #1467ff;
--input-line-error: var(--color-error);
}

@ -0,0 +1,31 @@
.mybar {
overflow: auto;
}
.mybar::-webkit-scrollbar {
width: 6px;
height: 6px;
color: transparent;
}
.mybar::-webkit-scrollbar-track {
border-radius: 3px;
}
.mybar::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: transparent;
}
.mybar:hover::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .2);
}
.mybar:hover::-webkit-scrollbar-thumb {
/*background: linear-gradient(to top, #fcf5ee, #faf7e6, #73d2f3, #eeeeee);*/
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .8);
}
.mybar::-webkit-scrollbar-track-piece {
background: transparent;
}

@ -0,0 +1,74 @@
import { modelsSimpleAuth } from '@/models'
export const R = {
// 应用管理配置权限
App: 'app',
// 用户管理和绑定应用权限
User: 'user',
// 权限资源定义权限
Resource: 'resource',
// 角色管理和绑定用户权限
Role: 'role',
// 权限管理和绑定角色权限
Auth: 'auth',
}
const level = {
None: 0,
Do: 1,
Part: 1,
Read: 2,
Create: 3,
Update: 4,
Delete: 5,
All: 6
}
class authLevel {
level = level.None
constructor(level: number) {
this.level = level
}
CanDo(): boolean {
return this.level >= level.Do
}
CanRead(): boolean {
return this.level >= level.Read
}
CanCreate(): boolean {
return this.level >= level.Create
}
CanUpdate(): boolean {
return this.level >= level.Update
}
CanDelete(): boolean {
return this.level >= level.Delete
}
CanDoAny(): boolean {
return this.level >= level.All
}
}
export class Auths {
readonly list: modelsSimpleAuth[]
constructor(auths: modelsSimpleAuth[]) {
this.list = auths
}
Get(name: string, rid: string): authLevel {
let l = level.None
for (let i of this.list) {
if (i.name == name && (!i.rid || i.rid === rid) && i.level > l) {
l = i.level
}
}
console.log(l)
return new authLevel(l)
}
}
export function NewAuths(a: modelsSimpleAuth[]) {
return new Auths(a)
}

@ -0,0 +1,33 @@
<template>
<div id="wx_reg"></div>
</template>
<script setup lang='ts'>
import {onMounted} from 'vue'
function goto(id: string, app: string, url: string, state?: number, href?: string) {
// eslint-disable-next-line
// @ts-ignore
window.WwLogin({
id: 'wx_reg',
appid: id,
agentid: app,
redirect_uri: encodeURIComponent(url),
state: state,
href: href
})
}
let aid = ''
let app = ''
let url = ''
onMounted(() => {
goto(aid, app, url, new Date().getTime())
})
</script>
<style scoped>
</style>

@ -0,0 +1,74 @@
<template>
<div class="core rounded-2xl p-3">
<div class="grid gap-4 grid-cols-5">
<div class="col-span-2">
<vavator style="--color: none" @click="Go" round size="4rem" :src="core.icon"></vavator>
</div>
<div class="col-span-3 grid grid-cols-1 items-center text-left">
<div class="h-10 flex items-center text-2xl italic font-bold">
{{ core.name }}
</div>
<span class="truncate">{{ core.des }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { withDefaults } from 'vue'
import { useRouter } from 'vue-router'
import api from '@/api'
import { modelsApp } from '@/models'
import vavator from '@/components/vavator'
import { useUserStore } from '@/store/user'
import msg from '@/msg'
let router = useRouter()
let store = useUserStore()
let props = withDefaults(
defineProps<{
core: modelsApp
}>(),
{},
)
function Go() {
switch (props.core.status) {
case 'ok':
router.push({ name: 'app.main', params: { uuid: props.core.UUID } })
return
case 'apply':
msg.Info('请等待管理员审批进入')
return
case 'deny':
msg.Warn('进入申请未通过')
return
case 'disabled':
msg.Warn('已被禁止使用')
return
}
api.app
.user(props.core.id)
.add(store.id)
.Start(
(e) => {
if (e.Status === 'ok') {
router.push({ name: 'app.main', params: { uuid: props.core.id } })
return
}
props.core.status = e.Status
msg.Info('已发起加入申请')
},
(e) => {
msg.Warn('加入失败: ' + e)
},
)
return
}
</script>
<style scoped>
.core {
width: 256px;
background: rgba(146, 145, 145, 0.1);
}
</style>

@ -0,0 +1,142 @@
<template>
<div>
<slot></slot>
<n-modal @after-leave="emit('update:modelValue', false)" v-model:show="modelValue">
<n-card class="w-4/5 md:w-1/2 rounded-2xl" :title="role.Name" :bordered="false" size="huge">
<template #header-extra>
<n-button @click="auths.push({ edit: true })">添加权限</n-button>
</template>
<div class="grid grid-cols-5 gap-1 gap-y-8" style="line-height: 34px">
<div>ID</div>
<div>作用资源</div>
<div>作用ID</div>
<div>权限等级</div>
<div></div>
<template :key="key" v-for="(item, key) in auths">
<template v-if="item.edit">
<div>{{ item.ID }}</div>
<div>
<n-select v-model:value="item.ResourceID" :options="RIDOptions" />
</div>
<div>
<n-input v-model:value="item.RUID"></n-input>
</div>
<div>
<n-select v-model:value="item.Level" :options="levelOptions()"></n-select>
</div>
<div>
<n-button @click="update(item)"></n-button>
</div>
</template>
<template v-else>
<div>{{ item.ID }}</div>
<div>{{ item.RID }}</div>
<div>{{ item.RUID }}</div>
<div>{{ item.Level }}</div>
<div>
<n-button @click="item.edit = true">编辑</n-button>
<n-button @click="del(item.ID, key)">删除</n-button>
</div>
</template>
</template>
</div>
<template #footer>
<n-alert type="warning">请谨慎操作 操作不当可能使所有用户无法正常使用</n-alert>
</template>
</n-card>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { modelsAuth, modelsResource, modelsRole } from '@/models'
import api from '@/api'
let props = withDefaults(
defineProps<{
uuid: string
role: modelsRole
res: modelsResource[]
modelValue: boolean
}>(),
{},
)
let emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void
}>()
let id = computed(() => {
return props.role.ID || 0
})
let value = computed(() => {
return props.modelValue
})
let auths = ref<modelsAuth[]>([])
let RIDOptions = computed(() => {
let l = []
for (let r of props.res) {
l.push({
label: r.Name,
value: r.ID,
})
}
return l
})
let levelOptions = () => {
let l = []
for (let i = 0; i < 7; i++) {
l.push({
label: i,
value: i,
})
}
return l
}
watch(value, () => {
if (id.value > 0 && props.modelValue) {
api
.auth(props.uuid)
.listOfRole(id.value)
.Start((e) => {
auths.value = e
})
}
})
function del(id: number, index: number) {
api
.auth(props.uuid)
.del(id)
.Start((e) => {
auths.value.splice(index, 1)
msg.success('删除成功')
})
}
function update(row: modelsAuth) {
if (row.ID > 0) {
api
.auth(props.uuid)
.update(row.ID, row.ResourceID, row.RUID, row.Level)
.Start((e) => {
Object.assign(row, e)
msg.success('更新成功')
})
} else {
api
.auth(props.uuid)
.create(row.ResourceID, null, id.value, row.RUID, row.Level)
.Start((e) => {
Object.assign(row, e)
msg.success('添加成功')
})
}
// @ts-ignore
row.edit = false
}
</script>
<style scoped></style>

@ -0,0 +1,103 @@
<template>
<div>
<slot></slot>
<n-modal @after-leave="emit('update:modelValue', false)" v-model:show="modelValue">
<n-card class="w-4/5 md:w-1/2 rounded-2xl" :title="role.Name" :bordered="false" size="huge">
<template #header-extra>
<UserSelect @selected="tmp = $event"></UserSelect>
<n-button @click="add"></n-button>
</template>
<div class="grid grid-cols-4 gap-1 gap-y-8" style="line-height: 34px">
<div>ID</div>
<div>昵称</div>
<div>用户名</div>
<div></div>
<template :key="key" v-for="(item, key) in users">
<div>{{ item.ID }}</div>
<div>{{ item.Nickname }}</div>
<div>{{ item.Username }}</div>
<div>
<n-button @click="del(item.ID, key)">删除</n-button>
</div>
</template>
</div>
<template #footer></template>
</n-card>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { modelsRole, modelsUser } from '@/models'
import { computed, ref, watch } from 'vue'
import api from '@/api'
import UserSelect from '@/components/userSelect.vue'
let props = withDefaults(
defineProps<{
uuid: string
role: modelsRole
modelValue: boolean
}>(),
{},
)
let emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void
}>()
let id = computed(() => {
return props.role.ID || 0
})
let value = computed(() => {
return props.modelValue
})
let users = ref<modelsUser[]>([])
function del(uid: number, index: number) {
props.role.UserCount--
api
.role(props.uuid)
.user(id.value)
.delete(uid)
.Start((e) => {
users.value.splice(index, 1)
msg.success('删除成功')
})
}
let tmp = ref<modelsUser>(null)
function add() {
if (tmp.value && tmp.value.ID > 0) {
api
.role(props.uuid)
.user(id.value)
.create(tmp.value.ID)
.Start((e) => {
let added = false
for (let u of users.value) {
if (u.ID === tmp.value.ID) {
added = true
}
}
if (!added) {
users.value.push(tmp.value)
props.role.UserCount++
}
})
}
}
watch(value, () => {
if (id.value > 0 && props.modelValue) {
api
.role(props.uuid)
.user(id.value)
.list()
.Start((e) => {
users.value = e
})
}
})
</script>
<style scoped></style>

@ -0,0 +1,64 @@
<template>
<div>
<slot></slot>
<n-modal @after-leave="emit('update:modelValue',false)" v-model:show="modelValue">
<n-card class="w-4/5 md:w-1/2 rounded-2xl" :title="res.ID > 0 ? res.Name:' '" :bordered="false"
size="huge">
<template #header-extra>{{ res.ID > 0 ? '编辑' : '创建' }}</template>
<div class="grid grid-cols-5 gap-1 gap-y-8" style="line-height: 34px">
<div>资源名</div>
<div class="col-span-4">
<n-input :disabled="res.ID> 0" v-model:value="res.Name"></n-input>
</div>
<div>资源描述</div>
<div class="col-span-4">
<n-input type="textarea" v-model:value="res.Des"></n-input>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<n-button class="mx-3" @click="emit('update:modelValue', false)">取消</n-button>
<n-button @click="update">{{ res.ID > 0 ? '更新' : '创建' }}</n-button>
</div>
</template>
</n-card>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import {modelsResource} from '@/models'
import api from '@/api'
let props = withDefaults(defineProps<{
res: modelsResource
modelValue: boolean
uuid: string
}>(), {
res: {} as any,
modelValue: false,
uuid: '',
})
let emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void
(e: 'ok', v: modelsResource)
}>()
function update() {
if (props.res.ID > 0) {
emit('update:modelValue', false)
api.resource(props.uuid).update(props.res.ID, props.res).Start(e => {
})
return
}
api.resource(props.uuid).create(props.res.Name, props.res.Des).Start(e => {
emit('ok', e)
emit('update:modelValue', false)
})
}
</script>
<style scoped>
</style>

@ -0,0 +1,77 @@
<template>
<div>
<slot></slot>
<n-modal @after-leave="emit('update:modelValue', false)" v-model:show="modelValue">
<n-card
class="w-4/5 md:w-1/2 rounded-2xl"
:title="res.ID > 0 ? res.Name : ' '"
:bordered="false"
size="huge"
>
<template #header-extra>{{ res.ID > 0 ? '编辑' : '创建' }}</template>
<div class="grid grid-cols-5 gap-1 gap-y-8" style="line-height: 34px">
<div>角色名</div>
<div class="col-span-4">
<n-input :disabled="res.ID > 0" v-model:value="res.Name"></n-input>
</div>
<div>角色标签</div>
<div class="col-span-4">
<n-input type="textarea" v-model:value="res.Tag"></n-input>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<n-button class="mx-3" @click="emit('update:modelValue', false)">取消</n-button>
<n-button @click="update">{{ res.ID > 0 ? '更新' : '创建' }}</n-button>
</div>
</template>
</n-card>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { modelsRole } from '@/models'
import api from '@/api'
let props = withDefaults(
defineProps<{
res: modelsRole
modelValue: boolean
uuid: string
}>(),
{
res: {} as any,
modelValue: false,
uuid: '',
},
)
let emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void
(e: 'ok', v: modelsRole)
}>()
function update() {
if (props.res.ID > 0) {
emit('update:modelValue', false)
api
.role(props.uuid)
.update(props.res.ID, props.res)
.Start((e) => {
msg.success('更新成功')
})
return
}
api
.role(props.uuid)
.create(props.res.Name, props.res.Tag)
.Start((e) => {
msg.success('添加成功')
emit('ok', e)
emit('update:modelValue', false)
})
}
</script>
<style scoped></style>

@ -21,7 +21,7 @@
leave-active-class="animate__slideOutUp"
>
<div
class="animate__animated"
class="animate__animated header"
v-if="!app.hideHeader"
bordered
style="height: 64px; line-height: 64px"
@ -37,8 +37,9 @@
glassdoor
</one-icon>
</div>
<div class="h-full" style="margin-left: 10px">
<span type="primary">统一认证系统</span>
<div class="h-full flex gap-1" style="">
<div style="width: 3px; height: 100%; background: var(--L0)"></div>
<span style="color: var(--L0)">统一认证系统</span>
</div>
<div class="flex-grow flex justify-center">
<span class="text-2xl" style="line-height: 64px">{{ app.title }}</span>
@ -46,8 +47,8 @@
<div class="h-full px-3">
<fullscreen v-model="isFullScreen" class="header-icon">fullscreen</fullscreen>
<div class="header-icon">
<one-icon @click="">
{{ app.isDark ? 'Daytimemode' : 'nightmode-fill' }}
<one-icon @click="app.toggle_theme()">
{{ app.isDark ? 'Daytimemode-fill' : 'night' }}
</one-icon>
</div>
<div class="header-icon" @click="app.hideHeader = true">
@ -60,14 +61,14 @@
</div>
</div>
</transition>
<div>
<div style="height: calc(100vh - 88px)">
<slot></slot>
</div>
</div>
<div
bordered
style="height: 24px; line-height: 24px"
class="flex justify-around px-3 text-gray-500 text-xs"
class="flex justify-around px-3 text-gray-500 text-xs header"
>
<span class="hover:text-black cursor-pointer" @click="$router.push({ name: 'about' })">
关于OA
@ -90,6 +91,9 @@ let isFullScreen = false
</script>
<style scoped>
.header {
background: var(--base-bg);
}
.header-icon {
display: inline-block;
font-size: 24px;

@ -0,0 +1,134 @@
<!--
* menu.vue
* Copyright (C) 2022 veypi <i@veypi.com>
* 2022-10-12 10:00
* Distributed under terms of the Apache license.
-->
<template>
<div class="w-full h-full relative">
<div class="left-menu px-2 py-4 h-full absolute" :style="dy_style.menu">
<div class="lm-conent overflow-hidden h-full w-full">
<slot name='menu'>
</slot>
</div>
<div @click="toggle" class='lm-icon'>
<div class="icon-top"></div>
<div class="icon-bot"></div>
</div>
</div>
<div class="main h-full absolute" :style="dy_style.main">
<slot>
</slot>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
const props = withDefaults(defineProps<{
modelValue?: boolean
width?: number,
}>(), {
modelValue: true,
width: 8,
})
const emit = defineEmits<{
(e: 'update:modelValue', data: boolean): void
}>()
const value = ref(props.modelValue)
const dy_style = computed(() => {
if (value.value) {
return {
menu: {
left: '0',
width: props.width + 'rem'
},
main: {
width: 'calc(100% - ' + props.width + 'rem)',
left: props.width + 'rem'
},
top: '12deg',
bot: '-12deg'
}
}
return {
menu: {
left: -props.width + 'rem',
width: props.width + 'rem'
},
main: {
width: '100%',
left: '0',
},
top: '-12deg',
bot: '12deg'
}
})
const toggle = () => {
value.value = !value.value
emit('update:modelValue', value.value)
}
</script>
<style scoped>
.left-menu {
transition: all 0.2s linear;
}
.lm-conent {
background: var(--base-bg-3);
position: relative;
}
.lm-icon {
cursor: pointer;
height: 72px;
width: 32px;
position: absolute;
top: calc(50% - 36px);
right: -28px;
transition: all 0.2s linear;
z-index: 10;
}
.lm-icon div {
background: #999;
transition: all 0.2s linear;
position: absolute;
width: 4px;
border-radius: 2px;
height: 38px;
left: 14px;
}
.lm-icon:hover div {
background: #777;
}
.lm-icon .icon-bot {
position: absolute;
top: 34px;
}
.lm-icon:hover .icon-top {
transform: rotate(v-bind('dy_style.top')) scale(1.15) translateY(-2px);
}
.lm-icon:hover .icon-bot {
transform: rotate(v-bind('dy_style.bot')) scale(1.15) translateY(2px);
}
.main {
transition: all 0.2s linear;
}
</style>

@ -0,0 +1,9 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2022-04-03 14:56
* @descriptionindex
*/
import index from './index.vue'
export default index

@ -0,0 +1,411 @@
<template>
<div :vtype="type"
:class="[hideBorder ? 'hide-hr' : '', tidy ? '' : 'no-tidy', flexy ? 'flex-col' : '', disabled ? 'cursor-not-allowed' : '']"
ref="all" class="my-input center flex justify-center items-center relative" :style="dy_style">
<div class="flex-shrink" :style="{ 'width': labelWidth }">
<slot name="label"></slot>
</div>
<template v-if="type === ArgType.Number">
<input :type="type" :disabled="disabled" @input="check()" :value="value" @focusout="update"
@focusin="change('input')" class="main w-full" :style="dy_style" style="font-weight: inherit"
ref="inputRef" @blur="update" @keyup.enter="update; unblur()">
</template>
<template v-else-if="type === ArgType.Text">
<input :type="type" :disabled="disabled" @input="check()" :value="value" @focusout="update"
@focusin="change('input');" class="main w-full" :style="dy_style" style="font-weight: inherit"
ref="inputRef" @blur="update" @keyup.enter="update">
</template>
<template v-else-if="type === ArgType.Password">
<input :type="type" :disabled="disabled" @input="check()" :value="value" @focusout="update"
@focusin="change('input');" class="main w-full" :style="dy_style" style="font-weight: inherit"
ref="inputRef" @blur="update" @keyup.enter="update">
</template>
<template v-else-if="type === ArgType.File">
<!-- <FormKit v-model="value" @change="update" class="main" type="file" outer-class="w-full" />-->
<!-- <FormKit class="main" type="file" outer-class="w-full" />-->
<div class="div-center rounded-md w-full relative" style="">
<slot name='file'>
{{ value == "" ? "no file chosen" : value }}
</slot>
<input class="absolute w-full h-full" type="file"
style="color:white;font-size: large;opacity: 0%;top:0;left:0" @change="choose_file($event)" />
</div>
<!-- @click="setParameter(v.key,vv.key, c.value)"-->
</template>
<template v-else-if="type === ArgType.Radio">
<template v-for="(ov, ok) in transDic">
<div :class="[value === ok ? 'radio-btn-active' : 'div-btn', flexy ? 'w-full' : '']"
@click="setSelect(ok)" style="color:white;height: 3rem"
class="div-center font-bold truncate radio-btn mx-8 rounded-md p-2 my-4 transition duration-500">
{{ ov }}
</div>
</template>
</template>
<template v-else-if="type === ArgType.Select">
<div class="main cursor-pointer w-full overflow-x-auto whitespace-nowrap" @click="showSelect"
:title="title">
<span v-if="!value"></span>
<span v-else-if="!Array.isArray(value)">{{ transDic[value] || value }}</span>
<template v-else>
<span class="mx-2" v-for="iv in value">{{ transDic[iv] || iv }}</span>
</template>
</div>
<div @mouseleave="showSelectOpt = false"
:style="{ left: selectPos[0] + 'px', top: selectPos[1] + 'px', height: showSelectOpt ? '20rem' : '0rem' }"
class="select-opt text-base text-white rounded-md overflow-y-auto" style="min-width: 10rem;"
:title="title">
<div class="m-2 p-2" v-if="!options"></div>
<div :class="[ok === value ? 'bg-gray-500' : 'bg-gray-800']"
class="cursor-pointer m-2 p-2 rounded-md hover:bg-gray-500" @click="setSelect(ok)"
v-for="(ov, ok) in transDic">
{{ ov }}
</div>
<div class="w-full h-32"></div>
</div>
</template>
<template v-else-if="type === ArgType.Region">
<div class="flex items-center justify-center">
<template v-if="value[0] !== '∞'">
<one-icon class="div-btn" @click="updateIndex(0, '∞')">kuohao</one-icon>
<input type="number" :disabled="disabled" @input="check()" v-model="value[0]" @focusout="update"
@focusin="change('input')" class="main w-1/3 text-center" @blur="update" @keyup.enter="update">
</template>
<template v-else>
<one-icon class="div-btn" @click="updateIndex(0, 0)">zuokuohao</one-icon>
<div class="w-1/3 flex justify-center items-center">
<one-icon>minus</one-icon>
<one-icon>infinite</one-icon>
</div>
</template>
<div>,</div>
<template v-if="value[1] !== '∞'">
<input type="number" :disabled="disabled" v-model="value[1]" @focusout="update"
@focusin="change('input')" class="main w-1/3 text-center" @blur="update" @keyup.enter="update">
<one-icon class="div-btn" @click="updateIndex(1, '∞')">kuohao-r</one-icon>
</template>
<template v-else>
<div class="w-1/3 flex justify-center items-center">
<one-icon>plus</one-icon>
<one-icon>infinite</one-icon>
</div>
<one-icon class="div-btn" @click="updateIndex(1, 1)">youkuohao</one-icon>
</template>
</div>
</template>
<template v-else-if="type === ArgType.Bool">
<div class="rounded-full relative overflow-x-hidden transition duration-300 cursor-pointer text-white leading-8"
@click="value = !value; update()" style='height: 2rem;width: 6rem;'
:style="{ 'background': value ? '#1467ff' : '#555' }">
<template v-if="value">
<slot name="ok"></slot>
</template>
<template v-else>
<slot name="no"></slot>
</template>
<div class="bool-bg rounded-full m-1" style="background: #fff;height: 1.5rem;width: 1.5rem;"
:style="{ 'transform': 'translateX(' + (value ? '4' : '0') + 'rem)' }">
</div>
</div>
</template>
<hr>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch, computed } from 'vue'
import { ArgType, Dict } from '@/models'
import validator from 'validator';
const props = withDefaults(defineProps<{
modelValue?: any
type?: ArgType,
options?: any,
disabled?: boolean
hideBorder?: boolean
tidy?: boolean
labelWidth?: string
align?: string
flexy?: boolean
require?: boolean
validator?: any
//
title?: string
}>(), {
modelValue: '',
type: ArgType.Text,
disabled: false,
hideBorder: false,
tidy: false,
align: '',
flexy: false,
require: false,
labelWidth: '4rem'
})
const emit = defineEmits<{
(e: 'update:modelValue', data: any): void
(e: 'change', data: any): void
(e: 'upload', data: any): void
}>()
const dy_style = computed(() => `text-align:${props.align}`)
let inputRef = ref<HTMLInputElement>()
let all = ref<HTMLElement>()
const transDic = ref({} as Dict)
const change = (s: string) => {
if (props.disabled) {
return
}
if (s === 'idle') {
all.value?.classList.remove('my-input-active')
all.value?.classList.remove('my-input-error')
return
} else if (s === 'input') {
all.value?.classList.add('my-input-active')
} else if (s === 'error') {
all.value?.classList.add('my-input-error')
}
}
const value = ref(props.modelValue)
const sync = () => {
if (typeof props.modelValue === 'object') {
value.value = JSON.parse(JSON.stringify(props.modelValue))
} else {
value.value = props.modelValue
}
if (props.type === ArgType.Number) {
let v = parseFloat(props.modelValue) || 0
}
if (props.type === ArgType.Radio || props.type === ArgType.Select) {
transDic.value = {}
if (Array.isArray(props.options)) {
for (let i of props.options) {
if (typeof i === 'string') {
transDic.value[i] = i
} else {
transDic.value[i.key] = i.name
}
}
} else {
for (let i in props.options) {
transDic.value[i] = props.options[i]
}
}
}
}
watch(props, sync)
const check = (e?: InputEvent) => {
if (props.type === ArgType.Number) {
let v = inputRef.value?.valueAsNumber
if (v !== 0 && !v) {
return false
}
if (typeof props.options?.max === 'number' && v > props.options.max) {
return false
}
if (typeof props.options?.min === 'number' && v < props.options.min) {
return false
}
value.value = v
} else if (props.type === ArgType.Region) {
if (value.value[0] !== '∞' && value.value[1] !== '∞' && value.value[0] >= value.value[1]) {
return false
}
} else if (props.type === ArgType.Text || props.type === ArgType.Password) {
value.value = inputRef.value?.value
if (!validator.isLength(value.value, props.options)) {
return false
}
}
if (typeof props.validator === 'function') {
if (!props.validator(value.value)) {
return false
}
}
return true
}
const update = () => {
if (check()) {
change('idle')
emit('update:modelValue', value.value)
emit('change', value.value)
} else {
change('error')
}
}
const updateIndex = (index: number, v: any) => {
if (props.disabled) {
return
}
value.value[index] = v
update()
}
onMounted(() => {
sync()
})
const showSelectOpt = ref(false)
const selectPos = ref([0, 0])
const showSelect = (e: MouseEvent) => {
if (props.disabled) {
return
}
selectPos.value[0] = e.clientX - 20
selectPos.value[1] = e.clientY - 20
showSelectOpt.value = true
}
const setSelect = (e: any) => {
showSelectOpt.value = false
if (Array.isArray(value.value)) {
for (let i in value.value) {
if (value.value[i] === e) {
value.value.splice(i, 1)
update()
return
}
}
value.value.push(e)
} else {
value.value = e
}
update()
}
function choose_file(e: any) {
var filename = String(e.target.files[0].name)
const h = filename.substring(filename.lastIndexOf('.') + 1)
if (filename.length > 25) {
value.value = filename.slice(0, 15) + "...\xa0\xa0\xa0." + h
}
else {
value.value = filename
}
emit('upload', e.target.files[0])
// if (resultFile) {
// var reader = new FileReader();
// reader.readAsText(resultFile);
// reader.onload = function (e) {
// let d = this.result
// };
//
// }
}
function unblur() {
inputRef.value?.blur()
}
</script>
<style lang="less" scoped>
.no-tidy {
padding: 0.5rem 2rem;
}
.my-input {
position: relative;
hr {
margin: auto;
position: absolute;
bottom: -1px;
width: calc(100% - 4rem);
left: 2rem;
border: var(--input-line-default) solid 1px;
//visibility: hidden;
transition: all 0.2s linear;
}
&:hover hr {
border: var(--input-line-shine) solid 1px;
width: 100%;
left: 0;
}
}
.hide-hr {
hr {
border: none !important;
width: 0;
left: 50%;
}
}
.my-input-active {
hr {
border: var(--input-line-shine) solid 1px;
width: 100%;
left: 0;
}
}
.my-input-error {
hr {
border: var(--input-line-error) solid 1px !important;
width: 100%;
left: 0;
}
}
.main {
border: none;
outline: none;
background: none;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
}
.select-opt {
z-index: 10;
position: fixed;
left: 0;
top: 0;
background: #333;
transform-origin: top;
transition: height 0.3s linear;
}
.radio-btn {
background: #A8A8A8;
min-height: 2.5rem;
}
.radio-btn-active {
background: #EF857D;
}
.bool-bg {
position: absolute;
height: 100%;
left: 0px;
bottom: 0px;
/* 渐变背景 ,自左到右 */
/* background: linear-gradient(135deg, #FF9D6C, #BB4E75); */
/* background: linear-gradient(to right, #f09819, #ff5858); */
/* 添加动画过渡.贝塞尔曲线 */
transition: 0.3s cubic-bezier(1, 0.05, 0.9, 0.9);
/* transition: left 3s linear; */
}
</style>

@ -0,0 +1,61 @@
<template>
<Menu>
<template #menu>
<slot name="sider"></slot>
</template>
<div class="mx-5">
<slot name="title"></slot>
<slot name="subtitle"></slot>
<div @click="go(item)" :key="key" v-for="(item, key) in breads">
<one-icon class="inline-block" v-if="item.Type === 'icon'">
{{ item.Name }}
</one-icon>
<span v-else>{{ item.Name }}</span>
</div>
<slot name="avatar"></slot>
<slot name="extra"></slot>
<slot name="footer"></slot>
<slot></slot>
</div>
</Menu>
</template>
<script lang="ts" setup>
import Menu from './menu.vue'
import { modelsBread } from '@/models'
import { useRoute, useRouter } from 'vue-router'
import { computed } from 'vue'
import { useAppStore } from '@/store/app'
let router = useRouter()
let route = useRoute()
let store = useAppStore()
let breads = computed(() => {
let list: modelsBread[] = []
for (let b of store.breads) {
list.push(b)
if (b.RName === route.name) {
break
}
}
return list
})
function go(b: modelsBread) {
router.push({
name: b.RName,
params: b.RParams,
query: b.RQuery,
})
}
function back() {
if (breads.value.length > 1) {
let b = breads.value[breads.value.length - 2]
router.push({ name: b.RName, query: b.RQuery, params: b.RParams })
}
}
</script>
<style scoped></style>

@ -0,0 +1,9 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2021-12-04 14:32
* @descriptionindex
*/
import upload from './uploader.vue'
export default upload

@ -0,0 +1,58 @@
<template>
<div @click="click">
<input ref="file" type="file" hidden @change="upload" />
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { createClient } from '@/libs/webdav'
import { useAppStore } from '@/store/app'
import { ref } from 'vue'
let store = useAppStore()
let file = ref(null)
let emits = defineEmits<{
(e: 'success', v: string): void
(e: 'failed'): void
}>()
let props = withDefaults(
defineProps<{
url: string
}>(),
{
url: '',
},
)
function click() {
file.value.dispatchEvent(new MouseEvent('click'))
}
let prefix = '/file/public/app/' + store.id + '/'
let client = createClient(prefix, { headers: { auth_token: localStorage.getItem('auth_token') } })
async function upload() {
let list = file.value.files
if (list.length) {
let reader = new FileReader()
reader.onload = function (event) {
var res = event.target.result
// let data = new Blob([res])
let url = props.url.replaceAll('.', '.' + new Date().getTime().toString() + '.')
client.putFileContents(url, res).then((e) => {
if (e) {
emits('success', prefix + url)
} else {
emits('failed')
}
})
}
reader.readAsArrayBuffer(list[0])
} else {
}
}
</script>
<style scoped></style>

@ -0,0 +1,54 @@
<template>
<div>
<n-select
filterable
placeholder="搜索用户"
:options="options"
:loading="loading"
clearable
remote
@search="handleSearch"
@update-value="select"
/>
</div>
</template>
<script lang="ts" setup>
import {ref} from 'vue'
import api from '@/api'
import {modelsUser} from '@/models'
let emits = defineEmits<{
(e: 'selected', v:modelsUser): void
}>()
let options = ref([])
let loading = ref(false)
function select(v, o) {
emits('selected', o.user)
}
function handleSearch(query: string) {
if (!query.length) {
options.value = []
return
}
loading.value = true
api.user.search(query).Start((e: modelsUser[]) => {
let l = []
for (let u of e) {
l.push({
label: u.Username,
value: u.ID,
user: u,
})
}
options.value = l
loading.value = false
})
}
</script>
<style scoped>
</style>

@ -0,0 +1,10 @@
/*
* index.ts
* Copyright (C) 2022 veypi <i@veypi.com>
* 2022-08-15 19:56
* Distributed under terms of the Apache license.
*/
import main from './index.vue'
export default main

@ -0,0 +1,39 @@
<!--
* index.vue
* Copyright (C) 2022 veypi <i@veypi.com>
* 2022-08-15 19:56
* Distributed under terms of the Apache license.
-->
<template>
<img class="v-avator" :src="props.src || defsrc" alt="" :onerror="onerror"
:style="{ height: props.size, width: props.size }">
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const defsrc = ref('/media/icon/404.svg')
const props = withDefaults(defineProps<{
src?: string
size?: string
}>(), {
size: '2rem'
})
const onerror = (e: any) => {
let img = e.srcElement;
img.src = defsrc.value;
img.onerror = null;
}
</script>
<style scoped>
.v-avator {
display: flex;
border-radius: 50%;
align-items: center;
justify-content: center;
overflow: hidden;
}
</style>

@ -0,0 +1,9 @@
/*
* index.ts
* Copyright (C) 2022 veypi <i@veypi.com>
* 2022-12-20 23:44
* Distributed under terms of the Apache license.
*/
import modal from './index.vue'
export default modal

@ -0,0 +1,50 @@
<!--
* index.vue
* Copyright (C) 2022 veypi <i@veypi.com>
* 2022-12-20 23:45
* Distributed under terms of the Apache license.
-->
<template>
<Teleport to="body">
<transition
mode="out-in"
enter-active-class="animate__fadeInUpBig"
leave-active-class="animate__fadeOutDownBig"
>
<div
v-if="modelValue"
class="vmodal flex justify-center items-center animate__animated"
@click.self="emit('update:modelValue', false)"
>
<div class="vcore rounded-lg">
<slot></slot>
</div>
</div>
</transition>
</Teleport>
</template>
<script lang="ts" setup>
let emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void
}>()
let props = defineProps<{
modelValue: boolean
}>()
</script>
<style scoped>
.vmodal {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: 100vw;
}
.vcore {
background: var(--base-bg-2);
width: 30rem;
height: 40rem;
}
</style>

@ -0,0 +1,33 @@
/*
* en.ts
* Copyright (C) 2022 veypi <i@veypi.com>
* 2022-08-15 17:53
* Distributed under terms of the Apache license.
*/
export default {
a: {
username: 'username',
password: 'password',
repeat_pass: 'repeat',
register: 'register',
login: 'login',
success: 'success',
failed: 'failed',
account: "account",
nickname: "nickname",
email: "email",
logout: "logout",
user: "user",
setting: "setting",
},
m: {
home: "home",
editor: "editor",
},
msg: {
e1: 'please conform your form',
}
}

@ -0,0 +1,26 @@
/*
* index.ts
* Copyright (C) 2022 veypi <i@veypi.com>
* 2022-08-15 17:39
* Distributed under terms of the Apache license.
*/
import util from '@/libs/util';
import { createI18n, useI18n } from 'vue-i18n'
// import { createI18n, useI18n } from 'vue-i18n/dist/vue-i18n.esm-bundler.js'
import en from './en'
import zh from './zh'
const messages = {
en, zh
}
export { useI18n }
export default createI18n({
locale: util.getCookie('language') || 'zh',
legacy: false,
silentTranslationWarn: true,
fallbackLocale: 'en', // set fallback locale
messages,
})

@ -0,0 +1,33 @@
/*
* zh.ts
* Copyright (C) 2022 veypi <i@veypi.com>
* 2022-08-15 17:53
* Distributed under terms of the Apache license.
*/
export default {
a: {
username: '用户名',
password: '密码',
repeat_pass: '重复密码',
register: '注册',
login: '登录',
success: '成功',
failed: '失败',
account: "我的账户",
nickname: "昵称",
email: "邮箱",
logout: "注销",
user: "用户",
setting: "设置",
},
m: {
home: "首页",
editor: "编辑",
},
msg: {
e1: '请完善表单',
},
'no access': '无权访问',
}

@ -1,14 +1,39 @@
@import "./assets/mybar.css";
@import "./assets/light.css";
@import "./assets/dark.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
.animate__400ms {
--animate-duration: 400ms;
}
.div-center {
@apply flex justify-center items-center;
}
.div-btn {
@apply cursor-pointer transition duration-500 ease-in-out transform hover:scale-110 hover:opacity-50;
}
.div-btn hr {
right: 50%;
position: absolute;
bottom: 1px;
width: 0;
border: var(--color-primary) solid 1px;
visibility: hidden;
transition: all 0.2s linear;
--base-color: #000;
--base-bg: #fff;
}
:root[theme=dark] {
--base-color: #fff;
--base-bg: #000;
.div-btn:hover {
/*border-bottom: #03a9f4 1px solid;*/
}
.div-btn:hover hr {
visibility: visible;
width: 80%;
right: 10%;
}

@ -3,12 +3,15 @@ import App from './App.vue'
import OneIcon from '@veypi/one-icon'
import router from '@/router'
import { createPinia } from 'pinia'
import i18n from '@/i18n'
import 'animate.css'
import './assets/icon.js'
import './msg/index.css'
import './index.css'
let app = createApp(App)
app.use(OneIcon)
app.use(router)
app.use(i18n)
app.use(createPinia())
app.mount('#app')

@ -0,0 +1,151 @@
/*
* @name: index
* @author: veypi <i@veypi.com>
* @date: 2021-11-18 17:36
* @descriptionindex
*/
export type Dict = { [key: string]: any }
export enum ArgType {
Text = 'text',
Password = 'password',
Bool = 'bool',
Select = 'select',
Radio = 'radio',
Number = 'number',
Region = 'region',
NumList = 'numList',
StrList = 'strList',
Table = 'table',
Grid = 'grid',
File = 'file',
Img = 'img'
}
export const ArgTypesTrans = {
[ArgType.Text]: '文本',
[ArgType.Password]: '密码',
[ArgType.Select]: '选择器',
[ArgType.Radio]: '单选框',
[ArgType.Number]: '数字',
[ArgType.Region]: '区间',
[ArgType.NumList]: '数组',
[ArgType.StrList]: '文本集合',
[ArgType.Table]: '表格',
[ArgType.Grid]: '矩阵',
[ArgType.File]: '文件',
[ArgType.Img]: '图片',
[ArgType.Bool]: '开关',
}
export interface modelsBread {
Index: number
Name: string
Type?: string
RName: string
RParams?: any
RQuery?: any
}
export interface modelsApp {
created: string
updated: string
delete_flag: boolean
des: string
hide: boolean
icon: string
id: string
name: string
redirect: string
role_id: string
status: number
user_count: number
// Creator: number
// Des: string
// EnableEmail: boolean
// EnablePhone: boolean
// EnableRegister: true
// EnableUser: boolean
// EnableUserKey: boolean
// EnableWx: boolean
// Hide: boolean
// Host: string
// Icon: string
// InitRole?: null
// InitRoleID: number
// Name: string
// UUID: string
// UserCount: number
// UserKeyUrl: string
// UserRefreshUrl: string
// UserStatus: string
// Users: null
}
export interface modelsUser {
// Index 前端缓存
Index?: number
Apps: modelsApp[]
Auths: null
CreatedAt: string
DeletedAt: null
ID: number
Icon: string
Position: string
Roles: null
Status: string
UpdatedAt: string
Username: string
Email: string
Nickname: string
Phone: string
}
export interface modelsSimpleAuth {
level: number
name: string
rid: string
}
export interface modelsAuth {
App?: modelsApp
AppUUID: string
CreatedAt: string
DeletedAt: null
ID: number
Level: number
RID: string
RUID: string
Resource?: modelsResource
ResourceID: number
Role?: modelsRole
RoleID: number
UpdatedAt: string
User?: modelsUser
UserID?: number
}
export interface modelsRole {
App?: modelsApp
AppUUID: string
Auths: null
CreatedAt: string
DeletedAt: null
ID: number
Name: string
Tag: string
UpdatedAt: string
UserCount: number
}
export interface modelsResource {
App?: modelsApp
AppUUID: string
CreatedAt: string
DeletedAt: null
Des: string
ID: number
Name: string
UpdatedAt: string
}

@ -0,0 +1,135 @@
/*
* index.css
* Copyright (C) 2022 veypi <veypi@qq.com>
*
* Distributed under terms of the MIT license.
*/
:root {
--msg-box-color: #fff;
--msg-box-info: #0a0;
--msg-box-warn: #f90;
--z-index: 99999;
--font-size: 1rem;
}
.v-msg-box {
color: var(--msg-box-color);
position: fixed;
z-index: var(--z-index);
left: 0;
top: 0;
padding-top: 5vh;
width: 100vw;
height: 100vh;
color: var(--msg-box-color);
font-size: var(--font-size);
line-height: var(--font-size);
display: flex;
flex-direction: column;
justify-content: flex-start;
user-select: none;
}
.v-msg-box-mask {
background: rgba(0, 0, 0, 0.2);
/* filter: blur(6px); */
}
.v-msg-box-none-event {
pointer-events: none;
}
.v-msg-item {
min-width: 10rem;
margin: 0 auto;
text-align: center;
padding: 0.5rem 2rem;
border-radius: 0.5rem;
margin-top: 0.5rem;
transition: filter 0.2s;
animation: item-show 0.4s 1;
}
.v-msg-item:hover {
filter: brightness(80%);
}
.v-msg-item-remove {
animation: item-remove 0.4s 1;
}
.v-msg-prompt {
margin: 0 auto;
text-align: center;
padding: 2rem 2rem;
border-radius: 0.5rem;
margin-top: 0.5rem;
transition: filter 0.2s;
/* animation: item-show 0.4s 0; */
background: #e9e9e9;
height: 12rem;
width: 25rem;
color: #000;
}
.v-msg-prompt .v-msg-title {
text-align: left;
font-size: 1.2rem;
}
.v-msg-prompt .v-msg-input {
background: none;
margin: 1rem;
font-size: 1.2rem;
color: #000;
width: 100%;
height: 3rem;
outline: none;
border-bottom: 2px solid #000;
border-top: 0px;
border-left: 0px;
border-right: 0px;
}
.v-msg-prompt .v-msg-ok {
width: 6rem;
height: 2rem;
line-height: 2rem;
font-size: 1.2rem;
float: right;
border-radius: 4px;
background: #4caf50;
}
.v-msg-info {
background: var(--msg-box-info);
}
.v-msg-warn {
background: var(--msg-box-warn);
}
@keyframes item-show {
from {
opacity: 0;
margin-top: 2.5rem;
transform: scaleX(0);
}
to {
opacity: 1;
margin-top: 0.5rem;
transform: scaleX(1);
}
}
@keyframes item-remove {
from {
opacity: 1;
margin-top: 0.5rem;
transform: scaleX(1);
}
to {
opacity: 0;
margin-top: -2.5rem;
transform: scaleX(0);
}
}

@ -0,0 +1,110 @@
/*
* index.ts
* Copyright (C) 2022 veypi <veypi@qq.com>
* 2022-05-27 18:06
* Distributed under terms of the MIT license.
*/
class Message {
private box: HTMLDivElement
private timeout: number
private id: string
constructor(id: string, timeout?: number) {
this.timeout = timeout || 1000
this.box = document.getElementById(id) as HTMLDivElement
this.id = id
if (!this.box) {
console.error('can not found element ' + id)
return
}
this.box.classList.add('v-msg-box')
this.box.classList.add('v-msg-box-none-event')
}
private base(text: string, classList: string[], timeout: number) {
let msg = document.createElement('div')
msg.classList.add('v-msg-item')
msg.classList.add(...classList)
msg.innerText = text
this.box.appendChild(msg)
setTimeout(() => {
msg.classList.add('v-msg-item-remove')
msg.onanimationend = () => {
this.box.removeChild(msg)
}
}, timeout)
}
Prompt(text: string, placeholder?: string, defaul?: string) {
let that = this
this.box.classList.remove('v-msg-box-none-event')
this.box.classList.add('v-msg-box-mask')
return new Promise(function(resolve, reject) {
let msg = document.createElement('div')
let title = createElement('div', ['v-msg-title'])
let input = createElement('input', ['v-msg-input']) as HTMLInputElement
input.placeholder = placeholder || ''
let btn = createElement('div', [])
let btn_ok = createElement('div', ['v-msg-ok'])
input.value = defaul || ''
btn_ok.innerText = "ok"
title.innerText = text
msg.classList.add('v-msg-prompt')
msg.appendChild(title)
msg.appendChild(input)
btn.appendChild(btn_ok)
msg.appendChild(btn)
that.box.appendChild(msg)
let cancel = () => {
msg.classList.add('v-msg-item-remove')
that.box.classList.add('v-msg-box-none-event')
that.box.classList.remove('v-msg-box-mask')
msg.onanimationend = () => {
that.box.removeChild(msg)
}
}
btn_ok.onclick = () => {
cancel()
resolve(input.value)
}
that.box.onclick = (e: any) => {
if (e.target.id === that.id) {
cancel()
reject()
}
}
}).catch(e => {
if (e) {
console.log(e)
}
})
}
Warn(text: string) {
this.base(text, ['v-msg-warn'], this.timeout + 1500)
}
Info(text: string) {
this.base(text, ['v-msg-info'], this.timeout + 500)
}
}
function createElement(name: string, classList: string[]) {
let d = document.createElement(name)
d.classList.add(...classList)
return d
}
export { Message }
let msg: Message
function defaultMessage() {
console.log('init message')
if (!msg) {
msg = new Message('v-msg')
}
return msg
}
export default defaultMessage()

@ -6,13 +6,20 @@
*/
import {createRouter, createWebHistory} from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
import { Auths, R } from '@/auth'
import { useAppStore } from '@/store/app'
import util from '@/libs/util'
import { useUserStore } from '@/store/user'
import msg from '@/msg'
declare module 'vue-router' {
interface RouteMeta {
title?: string
isAdmin?: boolean
requiresAuth: boolean
checkAuth?: (r?: RouteLocationNormalized) => boolean
checkAuth?: (a: Auths, r?: RouteLocationNormalized) => boolean
}
}
const router = createRouter({
@ -21,7 +28,90 @@ const router = createRouter({
{
path: '/',
name: 'home',
component: () => import('../views/home.vue'),
meta: {
requiresAuth: true,
},
component: () => import('@/views/home.vue'),
},
{
path: '/app/:uuid?',
component: () => import('@/views/app.vue'),
redirect: { name: 'app.main' },
children: [
{
path: 'main',
name: 'app.main',
meta: {
title: '首页',
requiresAuth: true,
},
component: () => import('@/views/app/main.vue'),
},
{
path: 'users',
name: 'app.users',
meta: {
title: '用户',
requiresAuth: true,
checkAuth: (a, r) => {
return a.Get(R.User, r?.params.uuid as string).CanRead()
},
},
component: () => import('@/views/app/users.vue'),
},
{
path: 'roles',
name: 'app.roles',
meta: {
title: '权限',
requiresAuth: true,
checkAuth: (a, r) => {
return a.Get(R.Role, r?.params.uuid as string).CanRead()
},
},
component: () => import('@/views/app/roles.vue'),
},
{
path: 'setting',
name: 'app.setting',
meta: {
title: '应用设置',
requiresAuth: true,
checkAuth: (a, r) => {
return a.Get(R.App, r?.params.uuid as string).CanUpdate()
},
},
component: () => import('@/views/app/setting.vue'),
},
],
},
{
path: '/user/setting',
name: 'user_setting',
meta: {
requiresAuth: true,
},
component: () => import('@/views/user_setting.vue'),
},
{
path: '/about',
name: 'about',
component: () => import('@/views/about.vue'),
},
{
path: '/wx',
name: 'wx',
component: () => import('@/views/wx.vue'),
},
{
path: '/login/:uuid?',
name: 'login',
component: () => import('@/views/login.vue'),
},
{
path: '/register/:uuid?',
name: 'register',
component: () => import('@/views/register.vue'),
},
{
path: '/:path(.*)',
@ -31,12 +121,30 @@ const router = createRouter({
],
})
let user: any = null
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && to.meta.checkAuth) {
// to.matched.some(record => record.meta.requiresAuth)
if (to.query.noh === '1') {
let app = useAppStore()
app.hideHeader = true
}
if (to.meta.requiresAuth && !util.checkLogin()) {
// 此路由需要授权,请检查是否已登录
// 如果没有,则重定向到登录页面
return {
name: 'login',
// 保存我们所在的位置,以便以后再来
query: {redirect: to.fullPath},
query: { redirect: to.fullPath },
}
}
if (to.meta.checkAuth) {
if (!user) {
user = useUserStore()
}
if (!to.meta.checkAuth(user.auth, to)) {
msg.Warn('无权访问')
return from
}
}
})

@ -5,16 +5,30 @@
* @descriptionuser
*/
import { modelsBread } from '@/models'
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
state: () => {
return {
id: '',
hideHeader: false,
title: '',
isDark: false,
breads: [] as modelsBread[],
}
},
actions: {
toggle_theme() {
this.isDark = !this.isDark
document.documentElement.setAttribute('theme', this.isDark ? 'dark' : '')
},
setBreads(b: modelsBread) {
let l = this.breads.length
for (let i = l; i < b.Index; i++) {
this.breads.push({} as modelsBread)
}
this.breads[b.Index] = b
},
},
})

@ -0,0 +1,11 @@
/*
* index.ts
* Copyright (C) 2022 veypi <i@veypi.com>
* 2022-12-20 18:15
* Distributed under terms of the Apache license.
*/
import { useAppStore } from './app'
import { useUserStore } from './user'
export { useAppStore, useUserStore }

@ -5,12 +5,20 @@
* @descriptionuser
*/
import {defineStore} from 'pinia'
import api from '@/api'
import { Auths, NewAuths } from '@/auth'
import { util } from '@/libs'
import { modelsUser } from '@/models'
import { Base64 } from 'js-base64'
import router from "@/router";
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => {
return {
id: 0,
auth: {} as Auths,
local: {} as modelsUser,
username: '',
}
},
@ -19,5 +27,27 @@ export const useUserStore = defineStore('user', {
this.id = 1
this.username = 'admin'
},
fetchUserData() {
let token = util.getToken()?.split('.');
if (!token || token.length !== 3) {
return false
}
let data = JSON.parse(Base64.decode(token[1]))
console.log(data)
if (data.id) {
this.auth = NewAuths(data.access)
console.log(this.auth)
api.user.get(data.id).Start(e => {
console.log(e)
this.id = e.id
}, e => {
this.logout()
})
}
},
logout() {
localStorage.removeItem('auth_token')
router.push({ name: 'login' })
}
},
})

@ -1,16 +1,24 @@
<style scoped>
</style>
<template>
<div>404</div>
<div class="flex justify-center items-center">
<div class="text-center text-xl">
<one-icon style="font-size: 200px">404</one-icon>
<span>
路径失效啦! {{count}}
</span>
</div>
</div>
</template>
<script lang="ts" setup>
import {useRouter} from 'vue-router'
import {onMounted, ref} from 'vue'
import {useRouter, useRoute} from 'vue-router'
import {onMounted, ref} from "vue";
const route = useRoute()
const router = useRouter()
let count = ref(1)
let count = ref(5)
onMounted(() => {
let timer = setInterval(() => {
console.log([route.path, route.params])
let timer = setInterval(()=> {
count.value--
if (count.value === 0) {
router.push('/')
@ -19,3 +27,6 @@ onMounted(() => {
}, 1000)
})
</script>
<style scoped>
</style>

@ -0,0 +1,12 @@
<template>
<div>
about
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

@ -0,0 +1,137 @@
<template>
<siderframe>
<template v-slot:avatar>
<n-avatar
style="--color: none"
@click="util.goto(app.host)"
:src="app.icon"
round
size="large"
></n-avatar>
</template>
<template #title>{{ app.name }}</template>
<template #subtitle>{{ app.des }}</template>
<template v-slot:sider>
<div class="grid grid-cols-1">
<div class="cursor-pointer" v-for="(item, key) in navRouter" :key="key">
<div
class="pl-4 text-lg my-4"
:style="{ color: isEqualRoute(item) ? '#88baea' : '#888' }"
@click="
router.push(Object.assign({}, item, { params: route.params, query: route.query }))
"
>
{{ item.meta.title }}
</div>
<template v-if="isEqualRoute(item) && nav && nav.length > 0">
<transition-group
appear
enter-active-class="animate__zoomIn"
leave-active-class="animate__zoomOut"
>
<div
@click="goAnchor(tt)"
class="pl-8 rounded my-0.5 animate__animated animate__400ms"
v-for="(tt, kk) in nav"
:key="kk"
>
{{ tt.innerText }}
</div>
</transition-group>
</template>
</div>
</div>
</template>
<router-view v-slot="{ Component }">
<transition
mode="out-in"
enter-active-class="animate__fadeInLeft"
leave-active-class="animate__fadeOutRight"
>
<component class="animate__animated animate__400ms" :is="Component" ref="main"></component>
</transition>
</router-view>
</siderframe>
</template>
<script lang="ts" setup>
import { elementScrollIntoView } from 'seamless-scroll-polyfill'
import { useRoute, useRouter, RouteRecord } from 'vue-router'
import { computed, onMounted, ref, provide, onBeforeUnmount } from 'vue'
import api from '@/api'
import Siderframe from '@/components/siderframe.vue'
import { modelsApp, modelsBread } from '@/models'
import util from '@/libs/util'
import { useUserStore, useAppStore } from '@/store'
import msg from '@/msg'
let my = useUserStore()
let local = useAppStore()
let route = useRoute()
let router = useRouter()
let uuid = computed(() => route.params.uuid)
let app = ref<modelsApp>({} as modelsApp)
provide('app', app)
provide('uuid', uuid)
let main = ref(null)
// @ts-ignore
let nav = computed(() => (main.value ? main.value.nav : []))
let navRouter = ref(buildRouter())
function buildRouter(): RouteRecord[] {
let navs: RouteRecord[] = []
for (let n of router.getRoutes()) {
if (n.name && (n.name as string).startsWith('app')) {
if (n.meta.checkAuth) {
if (n.meta.checkAuth(my.auth, route)) {
navs.push(n)
}
} else {
navs.push(n)
}
}
}
return navs
}
function isEqualRoute(r: any) {
return r.name === route.name
}
onMounted(() => {
if (uuid.value === '') {
router.push({ name: '404', params: { path: route.path } })
return
}
api.app.get(uuid.value as string).Start(
(e: modelsApp) => {
app.value = e
local.title = e.name
local.setBreads({
Index: 1,
Name: e.name,
RName: 'app.main',
RParams: { uuid: e.id },
} as modelsBread)
},
(e) => {
msg.Warn('获取应用数据失败: ' + (e.err || e))
router.push({ name: '404', params: { path: route.path } })
},
)
})
onBeforeUnmount(() => {
local.title = ''
})
function goAnchor(element: any) {
// safari not support
// element.scrollIntoView({
// behavior: "smooth"
// })
elementScrollIntoView(element, { behavior: 'smooth' })
}
</script>
<style scoped></style>

@ -0,0 +1,25 @@
<template>
<div>
{{uuid}}
<div :ref="el => nav[6]=el" class="my-80">123123</div>
<div :ref="el => nav[k-1]=el" class="mb-64 text-center" v-for="(k) in [1,2,3,4,5,6]" :key="k">
{{ k }}
</div>
</div>
</template>
<script lang="ts" setup>
import {inject, ref} from "vue";
let uuid = inject('uuid')
let app = inject('app')
let nav = ref([])
defineExpose({
nav
})
</script>
<style scoped>
</style>

@ -0,0 +1,190 @@
<template>
<div>
<div class="flex justify-between">
<h1 class="page-h1">角色管理</h1>
<div class="my-5 mr-10">
<EditorRole @ok="roles.push($event)" v-model="roleFlag" :res="tmp" :uuid="uuid">
<n-button
@click="
tmp = {}
roleFlag = true
"
>
添加角色
</n-button>
</EditorRole>
</div>
</div>
<RoleAuths :res="resources" v-model="raFlag" :uuid="uuid" :role="tmp"></RoleAuths>
<RoleUsers v-model="ruFlag" :uuid="uuid" :role="tmp"></RoleUsers>
<n-data-table :bordered="false" :columns="columns" :data="roles" />
<div class="flex justify-between">
<h1 class="page-h1">资源管理</h1>
<div class="my-5 mr-10">
<EditorRes @ok="resources.push($event)" v-model="trFlag" :res="tmp" :uuid="uuid">
<n-button
@click="
tmp = {}
trFlag = true
"
>
添加资源
</n-button>
</EditorRes>
</div>
</div>
<n-data-table
class="mb-96"
:bordered="false"
:data="resources"
:columns="resCols"
></n-data-table>
</div>
</template>
<script lang="ts" setup>
import { h, inject, onMounted, Ref, ref } from 'vue'
import api from '@/api'
import { modelsBread, modelsResource, modelsRole } from '@/models'
import { useRoute } from 'vue-router'
import EditorRes from '@/components/editor/resource.vue'
import EditorRole from '@/components/editor/role.vue'
import RoleAuths from '@/components/connectors/roleauths.vue'
import RoleUsers from '@/components/connectors/roleusers.vue'
import { useAppStore } from '@/store'
let local = useAppStore()
let route = useRoute()
let roles = ref<modelsRole[]>([])
let uuid = inject<Ref<string>>('uuid')
const columns = [
{ title: 'ID', key: 'ID', width: 50 },
{ title: '角色名', key: 'Name', width: 100, fixed: 'left' },
{ title: '标签', key: 'Tag', width: 100 },
{ title: '创建时间', key: 'CreatedAt', fixed: 'left' },
{ title: '绑定用户数', key: 'UserCount', fixed: 'left' },
{
title: '操作',
key: '',
render(row: modelsRole, index: number) {
return [
h(
NButton,
{
class: 'mr-1',
size: 'small',
onClick: () => {
raFlag.value = true
tmp.value = row
},
},
{ default: () => '权限' },
),
h(
NButton,
{
class: 'mr-1',
size: 'small',
onClick: () => {
ruFlag.value = true
tmp.value = row
},
},
{ default: () => '用户' },
),
h(
NButton,
{
class: 'mr-1',
size: 'small',
onClick: () => {
api
.role(uuid.value)
.delete(row.ID)
.Start((e) => {
roles.value.splice(index, 1)
})
},
},
{ default: () => '删除' },
),
]
},
},
]
onMounted(() => {
local.setBreads({
Index: 2,
Name: '权限',
RName: route.name,
RParams: route.params,
RQuery: route.query,
} as modelsBread)
api
.role(uuid.value)
.list()
.Start((e) => {
roles.value = e
})
api
.resource(uuid.value)
.list()
.Start((e) => {
resources.value = e
})
})
let resources = ref<modelsResource[]>([])
const resCols = [
{ title: 'ID', key: 'ID', width: 50 },
{ title: 'Name', key: 'Name', width: 200, fixed: 'left' },
{ title: '描述', key: 'Des' },
{
title: '操作',
key: '',
width: 200,
fixed: 'right',
render(row, i) {
return [
h(
NButton,
{
class: 'mr-1',
size: 'small',
onClick: () => {
trFlag.value = true
tmp.value = row
},
},
{ default: () => '编辑' },
),
h(
NButton,
{
class: 'mr-1',
size: 'small',
onClick: () => {
api
.resource(uuid.value)
.delete(row.ID)
.Start((e) => {
resources.value.splice(i, 1)
msg.success('删除成功')
})
},
},
{ default: () => '删除' },
),
]
},
},
]
let tmp = ref({})
let trFlag = ref(false)
let roleFlag = ref(false)
let raFlag = ref(false)
let ruFlag = ref(false)
</script>
<style scoped></style>

@ -0,0 +1,115 @@
<template>
<div class="flex justify-center">
<div
style="line-height: 48px"
class="inline-block mt-16 grid grid-cols-5 w-1/3 text-center gap-4"
>
<div>应用名</div>
<div class="col-span-4">
<n-input v-model:value="data.Name" @blur="update('Name')"></n-input>
</div>
<div>UUID</div>
<div class="col-span-4 select-all">
{{ data.UUID }}
</div>
<div>Key</div>
<div class="col-span-4">
<n-popconfirm @positive-click="getKey">
<template #trigger>
<n-button>获取</n-button>
</template>
获取key将导致之前的key失效 是否获取?
</n-popconfirm>
</div>
<div>logo</div>
<div class="col-span-4">
<uploader :url="uuid + '.ico'" @success="handleFinish">
<n-avatar size="large" round :src="data.Icon"></n-avatar>
</uploader>
</div>
<div>自主注册</div>
<div class="col-span-4">
<n-switch
@update:value="update('EnableRegister', $event)"
v-model:value="data.EnableRegister"
>
<template #checked>允许</template>
<template #unchecked>禁止</template>
</n-switch>
</div>
<div>应用简介</div>
<div class="col-span-4 text-left">
<n-input
:autosize="{ minRows: 3 }"
type="textarea"
@blur="update('Des')"
maxlength="256"
v-model:value="data.Des"
></n-input>
</div>
<div>项目首页</div>
<div class="col-span-4 text-left">
<n-input v-model:value="data.Host" @blur="update('Host')"></n-input>
</div>
<div>跳转地址</div>
<div class="col-span-3 text-left">
<n-input v-model:value="data.UserRefreshUrl" @blur="update('UserRefreshUrl')"></n-input>
</div>
<div class="col-span-1">
<span class="text-blue-500" @click="util.goto('/login?uuid=' + uuid)">GO</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { inject, watch, ref, onMounted } from 'vue'
import api from '@/api'
import util from '@/libs/util'
import { modelsApp } from '@/models'
import Uploader from '@/components/uploader'
let app = inject<{ value: modelsApp }>('app')
let uuid = inject('uuid')
let data = ref<modelsApp>({} as modelsApp)
function handleFinish(e: string) {
data.value.Icon = e
console.log(e)
update('Icon')
}
function update(key: string, v?: any) {
// @ts-ignore
if (v === undefined) {
v = data.value[key]
}
api.app.update(app.value.UUID, { [key]: v }).Start(
(e) => {
msg.success('更新成功')
app.value[key] = v
},
(e) => {
data.value[key] = app.value[key]
},
)
}
function sync() {
Object.assign(data.value, app.value)
}
watch(app, sync)
onMounted(sync)
function getKey() {
api.app.getKey(data.value.UUID).Start((e) => {
dialog.success({
title: '请保存好秘钥',
content: e,
})
})
}
</script>
<style scoped></style>

@ -0,0 +1,202 @@
<template>
<div>
<div class="flex justify-between">
<h1 class="page-h1">用户名单</h1>
<div class="my-5 mr-10">
<n-button
@click="
temp_user = {}
tu_flag = true
"
>
添加用户
</n-button>
</div>
</div>
<n-data-table :bordered="false" :columns="columns" :scroll-x="980" :data="users" />
<n-modal v-model:show="tu_flag">
<n-card
class="w-4/5 md:w-96 rounded-2xl"
:title="temp_user.Index >= 0 ? temp_user.Username : ' '"
:bordered="false"
size="huge"
>
<template #header-extra>{{ temp_user.Index >= 0 ? '编辑' : '创建' }}</template>
<div class="grid grid-cols-5 gap-1 gap-y-8" style="line-height: 34px">
<div>用户名</div>
<div class="col-span-4">
<n-input v-model:value="temp_user.Username"></n-input>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<n-button class="mx-3" @click="tu_flag = false">取消</n-button>
<n-button>更新</n-button>
</div>
</template>
</n-card>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { inject, onMounted, ref, h, computed, Ref } from 'vue'
import api from '@/api'
import { R } from '@/auth'
import { modelsBread, modelsUser } from '@/models'
import { useRoute } from 'vue-router'
import { useAppStore } from '@/store'
let local = useAppStore()
let route = useRoute()
let uuid = inject<Ref<string>>('uuid')
let users = ref<modelsUser[]>([])
let isOA = computed(() => uuid.value === local.id)
onMounted(() => {
if (isOA) {
columns.value.push({
title: '操作',
key: '',
width: 200,
render(row, index) {
return [
h(
nbtn,
{
onClick: () => {
temp_user.value = Object.assign({ Index: index }, row.User)
tu_flag.value = true
},
},
{
default: () => '编辑',
},
),
]
},
})
}
api.app
.user(uuid.value as string)
.list(0)
.Start((e) => {
users.value = e
})
})
let columns = ref([
{
title: 'ID',
key: 'UserID',
width: 100,
},
{
title: '用户',
key: 'User.Username',
width: 100,
fixed: 'left',
},
{
title: '加入时间',
key: 'User.CreatedAt',
},
{
title: 'Status',
key: 'Status',
width: 100,
render(row) {
let t = statusTag(row.Status)
// @ts-ignore
return h(
ntag,
{
type: t[1],
onClick: () => {
changeStatus(row)
},
},
{
default: () => t[0],
},
)
},
},
])
function statusTag(s: string) {
switch (s) {
case 'ok':
return ['正常', 'success']
case 'apply':
return ['申请中', 'info']
case 'deny':
return ['拒绝', '']
case 'disabled':
return ['禁用', 'warning']
}
return ['未知', '']
}
function changeStatus(u) {
if (store.state.user.auth.Get(R.User, uuid.value).CanUpdate()) {
dialog.warning({
title: '请选择切换状态',
content: () => {
let tags = []
for (let s of ['ok', 'apply', 'deny', 'disabled']) {
let t = statusTag(s)
if (u.Status !== s) {
// @ts-ignore
tags.push(
h(
ntag,
{
type: t[1],
onClick: () => {
api.app
.user(uuid.value)
.update(u.UserID, s)
.Start((e) => {
u.Status = s
dialog.destroyAll()
})
},
},
{
default: () => t[0],
},
),
)
}
}
return h(
'div',
{
class: 'flex justify-between mx-16 mt-10',
},
{
default: () => tags,
},
)
},
})
}
}
let temp_user = ref<modelsUser>({} as modelsUser)
let tu_flag = ref(false)
function add_user() {}
local.setBreads({
Index: 2,
Name: '用户',
RName: route.name,
RParams: route.params,
RQuery: route.query,
} as modelsBread)
</script>
<style scoped></style>

@ -1,6 +1,10 @@
<style scoped>
</style>
<template>
<div></div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

@ -0,0 +1,10 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

@ -1,7 +1,142 @@
<style scoped>
</style>
<template>
<div @click="$router.push('/123')">home</div>
<div>
<div>
<div class="flex justify-between">
<h1 class="page-h1">我的应用</h1>
<div class="my-5 mr-10">
<div @click="new_flag = true" v-if="store.auth.Get(R.App, '').CanCreate()"></div>
</div>
</div>
<div
class="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center"
>
<div v-for="(item, k) in ofApps" class="flex items-center justify-center" :key="k">
<AppCard :core="item"></AppCard>
</div>
</div>
</div>
<div class="mt-20" v-if="apps.length > 0">
<h1 class="page-h1">应用中心</h1>
<div
class="grid gap-4 grid-cols-1 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">
<AppCard :core="item"></AppCard>
</div>
</div>
</div>
<vmodal v-model="new_flag">
<div class="h-full w-full flex py-8 px-4 flex-col">
<div>
<span>应用名</span>
<myinput v-model="temp_app.name" />
</div>
<div>
<span>头像</span>
<uploader
url="test.ico"
@success="
(e) => {
temp_app.icon = e
}
"
>
<vavator size="3rem" round :src="temp_app.icon"></vavator>
</uploader>
</div>
<div class="grow"></div>
<div class="flex justify-end">
<div class="mx-3" @click="new_flag = false">取消</div>
<div @click="create_new"></div>
</div>
</div>
</vmodal>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import api from '@/api'
import AppCard from '@/components/app.vue'
import { R } from '@/auth'
import { modelsApp } from '@/models'
import Uploader from '@/components/uploader'
import { useUserStore } from '@/store/user'
import msg from '@/msg'
import vmodal from '@/components/vmodal'
import myinput from '@/components/myinput'
import vavator from '@/components/vavator'
let store = useUserStore()
let apps = ref<modelsApp[]>([])
let ofApps = ref<modelsApp[]>([])
function getApps() {
api.app.list().Start((e) => {
apps.value = e
api.app
.user('')
.list(store.id)
.Start(
(e) => {
ofApps.value = []
for (let i in e) {
let ai = apps.value.findIndex((a) => a.UUID === e[i].AppUUID)
if (ai >= 0) {
apps.value[ai].UserStatus = e[i].Status
if (e[i].Status === 'ok') {
ofApps.value.push(apps.value[ai])
apps.value.splice(ai, 1)
}
}
}
},
() => {},
)
})
}
onMounted(() => {
getApps()
})
let new_flag = ref(false)
let temp_app = ref({
name: '',
icon: '',
})
let form_ref = ref(null)
let rules = {
name: [
{
required: true,
validator(r: any, v: any) {
return (v && v.length >= 2 && v.length <= 16) || new Error('长度要求2~16')
},
trigger: ['input', 'blur'],
},
],
}
function create_new() {
// @ts-ignore
form_ref.value.validate((e: any) => {
if (!e) {
api.app.create(temp_app.value.name, temp_app.value.icon).Start(
(e) => {
e.Status = 'ok'
ofApps.value.push(e)
msg.Info('创建成功')
new_flag.value = false
},
(e) => {
msg.Warn('创建失败: ' + e)
},
)
}
})
}
</script>
<style scoped></style>

@ -0,0 +1,155 @@
<template>
<div class="flex items-center justify-center h-full w-full">
<div class="box px-10 pb-9 pt-16 rounded-xl w-96 relative">
<Myinput
:type="ArgType.Text"
label-width="8rem"
v-model="data.username"
:options="{ min: 5, max: 16 }"
>
<template #label>{{ $t('a.username') }}</template>
</Myinput>
<Myinput
:type="ArgType.Password"
label-width="8rem"
v-model="data.password"
:options="{ min: 6, max: 16 }"
>
<template #label>{{ $t('a.password') }}</template>
</Myinput>
<div class="flex justify-around mt-4">
<div
class="div-btn px-4 py-1 rounded"
style="background: var(--color-primary)"
@click="login"
>
{{ $t('a.login') }}
</div>
<div
class="div-btn px-4 py-1 rounded"
style="background: var(--color-primary)"
@click="router.push({ name: 'register' })"
>
{{ $t('a.register') }}
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, computed } from 'vue'
import api from '@/api'
import { useRoute, useRouter } from 'vue-router'
import msg from '@/msg'
import Myinput from '@/components/myinput'
import { ArgType } from '@/models'
import { useI18n } from 'vue-i18n'
import { useUserStore } from '@/store/user'
import { util } from '@/libs'
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
const u = useUserStore()
let data = ref({
username: '',
password: '',
})
function login2() {
// @ts-ignore
api.user.login(data.value.username, data.value.password).Start(
(headers: any) => {
if ('auth_token' in headers) {
localStorage.auth_token = headers.auth_token
u.init_user()
// store.commit('user/refreshToken', localStorage.auth_token)
msg.Info(t('a.success'))
// store.dispatch('user/fetchUserData')
let url = route.query.redirect || headers.redirect || '/'
router.push(url)
} else {
msg.Info('正在申请加入,请等待管理员审批')
}
},
(e) => {
console.log(e)
msg.Warn(t('a.failed') + '' + e.headers.error)
},
)
}
function login() {
// @ts-ignore
api.user.login(data.value.username, data.value.password).Start(
(headers: any) => {
if ('auth_token' in headers) {
localStorage.auth_token = headers.auth_token
// store.commit('user/freshToken', localStorage.auth_token)
msg.Info('登录成功')
// store.dispatch('user/fetchUserData')
let url = route.query.redirect || headers.redirect || '/'
redirect(url)
} else {
msg.Info('正在申请加入,请等待管理员审批')
}
console.log(headers)
},
(e) => {
console.log(e)
msg.Warn('登录失败:' + e.headers.error)
},
)
}
function redirect(url?: string) {
if (uuid.value && uuid.value !== local.id) {
api.app.get(uuid.value as string).Start(
(app: modelsApp) => {
api
.token(uuid.value as string)
.get()
.Start((e) => {
if (!url) {
url = app.UserRefreshUrl
}
e = encodeURIComponent(e)
url = url.replaceAll('$token', e)
window.location.href = url
})
},
(e) => {},
)
} else if (util.checkLogin()) {
if (url) {
router.push(url)
} else {
router.push({ name: 'home' })
}
}
}
let uuid = computed(() => {
return route.query.uuid
})
let ifLogOut = computed(() => {
return route.query.logout === '1'
})
onMounted(() => {
if (!ifLogOut.value) {
redirect()
}
// if (divs.value[0]) {
// // @ts-ignore
// divs.value[0].focus()
// }
})
</script>
<style scoped>
.box {
background: linear-gradient(145deg, var(--base-bg-3), var(--base-bg-2));
box-shadow: 20px 20px 60px var(--base-bg-3), -20px -20px 60px var(--base-bg-2);
}
</style>

@ -0,0 +1,94 @@
<template>
<div class="flex items-center justify-center h-full w-full">
<div class="box px-10 pb-9 pt-16 rounded-xl w-96 relative" style="">
<one-icon class="absolute text-5xl top-4 left-4" @click="$router.push({ name: 'login' })">
back
</one-icon>
<Myinput
:type="ArgType.Text"
label-width="8rem"
v-model="data.username"
:options="{ min: 5, max: 16 }"
>
<template #label>{{ $t('a.username') }}</template>
</Myinput>
<Myinput
:type="ArgType.Password"
label-width="8rem"
v-model="data.password"
:options="{ min: 6, max: 16 }"
>
<template #label>{{ $t('a.password') }}</template>
</Myinput>
<Myinput
:type="ArgType.Password"
label-width="8rem"
v-model="data.pass"
:validator="validatefc.pass"
>
<template #label>{{ $t('a.repeat_pass') }}</template>
</Myinput>
<div class="flex justify-around mt-4">
<div
class="div-btn px-4 py-1 rounded"
style="background: var(--color-primary)"
@click="register"
>
{{ $t('a.register') }}
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import api from '@/api'
import { useRouter } from 'vue-router'
import msg from '@/msg'
import Myinput from '@/components/myinput'
import { ArgType } from '@/models'
import validator from 'validator'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const router = useRouter()
let data = ref({
username: '',
password: '',
pass: '',
})
const validatefc = {
pass(u: string) {
return validator.equals(u, data.value.password)
},
}
function register() {
if (data.value.username && data.value.password && data.value.password === data.value.pass) {
// @ts-ignore
api.user.register(data.value.username, data.value.password).Start(
(url: string) => {
msg.Info(t('a.register') + t('a.success'))
router.push({ name: 'login' })
},
(e) => {
console.log(e)
msg.Warn('注册失败:' + e.headers.error)
},
)
} else {
msg.Warn(t('msg.e1'))
}
}
onMounted(() => {})
</script>
<style scoped>
.box {
background: linear-gradient(145deg, var(--base-bg-3), var(--base-bg-2));
box-shadow: 20px 20px 60px var(--base-bg-3), -20px -20px 60px var(--base-bg-2);
}
</style>

@ -0,0 +1,135 @@
<template>
<div class="pt-10">
<div class="flex justify-center">
<div
class="relative rounded-xl text-lg text-black"
:style="{ background: IsDark ? '#555' : '#d5d5d5' }"
>
<div
@click="ifInfo = true"
class="inline-block px-5 rounded-xl"
:style="{ background: ifInfo ? '#fc0005' : '' }"
>
个人信息
</div>
<div
@click="ifInfo = false"
class="inline-block px-5 rounded-xl"
:style="{ background: ifInfo ? '' : '#fc0005' }"
>
账户管理
</div>
</div>
</div>
<div class="inline-block flex justify-center mt-10">
<transition
mode="out-in"
enter-active-class="animate__fadeInLeft"
leave-active-class="animate__fadeOutRight"
>
<div v-if="ifInfo" class="animate__animated animate__faster">
<n-form label-placement="left" label-width="80px" label-align="left">
<n-form-item label="昵称">
<n-input v-model:value="user.Nickname" @blur="update('Nickname')"></n-input>
</n-form-item>
<n-form-item label="头像">
<uploader :url="user.ID + '.ico'" @success="handleFinish">
<n-avatar size="large" round :src="user.Icon"></n-avatar>
</uploader>
</n-form-item>
</n-form>
</div>
<div v-else class="animate__animated animate__faster">
<n-form label-align="left" label-width="80px" label-placement="left">
<n-form-item label="Username">
<n-input disabled v-model:value="user.Username"></n-input>
</n-form-item>
<n-form-item label="phone">
<n-input v-model:value="user.Phone" @blur="update('Phone')"></n-input>
</n-form-item>
<n-form-item label="email">
<n-auto-complete
:options="emailOptions"
v-model:value="user.Email"
@blur="update('Email')"
></n-auto-complete>
</n-form-item>
<n-form-item label="邮件通知">
<n-switch>
<template #checked>启用</template>
<template #unchecked>关闭</template>
</n-switch>
</n-form-item>
<n-form-item label="短信通知">
<n-switch>
<template #checked>启用</template>
<template #unchecked>关闭</template>
</n-switch>
</n-form-item>
</n-form>
</div>
</transition>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import api from '@/api'
import { modelsUser } from '@/models'
import Uploader from '@/components/uploader'
import { useUserStore } from '@/store/user'
import msg from '@/msg'
let store = useUserStore()
let ifInfo = ref(true)
let user = ref<modelsUser>({
ID: store.id,
Username: store.local.Username,
Nickname: store.local.Nickname,
Icon: store.local.Icon,
Email: store.local.Email,
Phone: store.local.Phone,
} as modelsUser)
let emailOptions = computed(() => {
return ['@qq.com', '@163.com', '@gmail.com', '@outlook.com', '@icloud.com', '@169.com'].map(
(suffix) => {
const prefix = user.value.Email.split('@')[0]
return {
label: prefix + suffix,
value: prefix + suffix,
}
},
)
})
function handleFinish(e: string) {
console.log(e)
user.value.Icon = e
update('Icon')
return
}
function update(key: string) {
// @ts-ignore
let v = user.value[key]
// @ts-ignore
if (v === store.local[key]) {
return
}
api.user.update(store.id, { [key]: v }).Start(
(e) => {
msg.Info('更新成功')
// @ts-ignore
store.local[key] = v
},
(e) => {
msg.Warn('更新失败: ' + e)
},
)
}
</script>
<style scoped></style>

@ -0,0 +1,51 @@
<template>
<div class='home d-flex justify-center align-center'>
<wx-login v-if="enable" :aid="aid" :app="agentID" :url="url"></wx-login>
</div>
</template>
<script setup lang='ts'>
import WxLogin from '@/components/WxLogin.vue'
import {computed, onMounted} from "vue";
import {useRoute} from 'vue-router'
import api from '@/api'
let route = useRoute()
let aid = ''
let agentID = ''
let url = ''
let uuid = computed(() => {
return route.query.uuid
})
let enable = computed(() => {
return uuid && aid && agentID && url
})
let code = computed(() => {
return route.query.code
})
let state = computed(() => {
return route.query.state
})
let msg = computed(() => {
return route.query.msg
})
onMounted(() => {
if (msg) {
console.log(msg)
alert(msg)
}
})
if (uuid) {
api.app.get(uuid.value as string).Start(e => {
url = e.wx.url + '/api/wx/login/' + uuid
aid = e.wx.corp_id
agentID = e.wx.agent_id
})
}
</script>
<style scoped>
</style>

@ -1,6 +1,6 @@
import {defineConfig} from 'vite'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import {resolve} from 'path'
import { resolve } from 'path'
// https://vitejs.dev/config/
export default defineConfig({
@ -14,12 +14,12 @@ export default defineConfig({
port: 3000,
proxy: {
'/api': {
target: 'http://127.0.0.1:4000/',
target: 'http://127.0.0.1:4001/',
changeOrigin: true,
ws: true,
},
'/media': {
target: 'http://127.0.0.1:4000/',
target: 'http://127.0.0.1:4001/',
changeOrigin: true,
ws: true,
},

@ -12,6 +12,44 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==
"@intlify/core-base@9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.2.2.tgz#5353369b05cc9fe35cab95fe20afeb8a4481f939"
integrity sha512-JjUpQtNfn+joMbrXvpR4hTF8iJQ2sEFzzK3KIESOx+f+uwIjgw20igOyaIdhfsVVBCds8ZM64MoeNSx+PHQMkA==
dependencies:
"@intlify/devtools-if" "9.2.2"
"@intlify/message-compiler" "9.2.2"
"@intlify/shared" "9.2.2"
"@intlify/vue-devtools" "9.2.2"
"@intlify/devtools-if@9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@intlify/devtools-if/-/devtools-if-9.2.2.tgz#b13d9ac4b4e2fe6d2e7daa556517a8061fe8bd39"
integrity sha512-4ttr/FNO29w+kBbU7HZ/U0Lzuh2cRDhP8UlWOtV9ERcjHzuyXVZmjyleESK6eVP60tGC9QtQW9yZE+JeRhDHkg==
dependencies:
"@intlify/shared" "9.2.2"
"@intlify/message-compiler@9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.2.2.tgz#e42ab6939b8ae5b3d21faf6a44045667a18bba1c"
integrity sha512-IUrQW7byAKN2fMBe8z6sK6riG1pue95e5jfokn8hA5Q3Bqy4MBJ5lJAofUsawQJYHeoPJ7svMDyBaVJ4d0GTtA==
dependencies:
"@intlify/shared" "9.2.2"
source-map "0.6.1"
"@intlify/shared@9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.2.2.tgz#5011be9ca2b4ab86f8660739286e2707f9abb4a5"
integrity sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==
"@intlify/vue-devtools@9.2.2":
version "9.2.2"
resolved "https://registry.yarnpkg.com/@intlify/vue-devtools/-/vue-devtools-9.2.2.tgz#b95701556daf7ebb3a2d45aa3ae9e6415aed8317"
integrity sha512-+dUyqyCHWHb/UcvY1MlIpO87munedm3Gn6E9WWYdWrMuYLcoIoOEVDWSS8xSwtlPU+kA+MEQTP6Q1iI/ocusJg==
dependencies:
"@intlify/core-base" "9.2.2"
"@intlify/shared" "9.2.2"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@ -126,7 +164,7 @@
"@vue/compiler-dom" "3.2.45"
"@vue/shared" "3.2.45"
"@vue/devtools-api@^6.4.5":
"@vue/devtools-api@^6.2.1", "@vue/devtools-api@^6.4.5":
version "6.4.5"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.4.5.tgz#d54e844c1adbb1e677c81c665ecef1a2b4bb8380"
integrity sha512-JD5fcdIuFxU4fQyXUu3w2KpAJHzTVdN+p4iOX2lMWSHMOoQdMAcpFLZzm9Z/2nmsoZ1a96QEhZ26e50xLBsgOQ==
@ -216,6 +254,11 @@ arg@^5.0.2:
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
autoprefixer@^10.4.4:
version "10.4.13"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.13.tgz#b5136b59930209a321e9fa3dca2e7c4d223e83a8"
@ -228,11 +271,37 @@ autoprefixer@^10.4.4:
picocolors "^1.0.0"
postcss-value-parser "^4.2.0"
axios@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.2.1.tgz#44cf04a3c9f0c2252ebd85975361c026cb9f864a"
integrity sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base-64@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a"
integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@ -260,6 +329,11 @@ caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001439.tgz#ab7371faeb4adff4b74dad1718a6fd122e45d9cb"
integrity sha512-1MgUzEkoMO6gKfXflStpYgZDlFM7M/ck/bgfVCACO5vnAf0fXoNVHdWtqGU+MYca+4bL9Z5bpOVmR33cWW9G2A==
charenc@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==
chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
@ -280,6 +354,25 @@ color-name@^1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
copy-anything@^2.0.1:
version "2.0.6"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.6.tgz#092454ea9584a7b7ad5573062b2a87f5900fc480"
integrity sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==
dependencies:
is-what "^3.14.1"
crypt@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@ -290,11 +383,23 @@ csstype@^2.6.8:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e"
integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==
debug@^3.2.6:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
dependencies:
ms "^2.1.1"
defined@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.1.tgz#c0b9db27bfaffd95d6f61399419b893df0f91ebf"
integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
detective@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034"
@ -319,6 +424,13 @@ electron-to-chromium@^1.4.251:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592"
integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==
errno@^0.1.1:
version "0.1.8"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
dependencies:
prr "~1.0.1"
esbuild-android-64@0.14.54:
version "0.14.54"
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"
@ -467,6 +579,13 @@ fast-glob@^3.2.12:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-xml-parser@^3.19.0:
version "3.21.1"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz#152a1d51d445380f7046b304672dd55d15c9e736"
integrity sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==
dependencies:
strnum "^1.0.4"
fastq@^1.6.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.14.0.tgz#107f69d7295b11e0fccc264e1fc6389f623731ce"
@ -481,6 +600,20 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
fraction.js@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950"
@ -510,6 +643,11 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
graceful-fs@^4.1.2:
version "4.2.10"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
@ -517,6 +655,28 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hot-patcher@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/hot-patcher/-/hot-patcher-0.5.0.tgz#9d401424585aaf3a91646b816ceff40eb6a916b9"
integrity sha512-2Uu2W0s8+dnqXzdlg0MRsRzPoDCs1wVjOGSyMRRaMzLDX4bgHw6xDYKccsWafXPPxQpkQfEjgW6+17pwcg60bw==
iconv-lite@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
image-size@~0.5.0:
version "0.5.5"
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@ -524,6 +684,11 @@ is-binary-path@~2.1.0:
dependencies:
binary-extensions "^2.0.0"
is-buffer@~1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-core-module@^2.9.0:
version "2.11.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144"
@ -548,6 +713,38 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-what@^3.14.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==
js-base64@^3.7.3:
version "3.7.3"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.3.tgz#2e784bb0851636bf1e99ef12e4f3a8a8c9b7639f"
integrity sha512-PAr6Xg2jvd7MCR6Ld9Jg3BmTcjYsHEBx1VlwEwULb/qowPf5VD9kEMagj23Gm7JRnSvE/Da/57nChZjnvL8v6A==
layerr@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/layerr/-/layerr-0.1.2.tgz#16c8e7fb042d3595ab15492bdad088f31d7afd15"
integrity sha512-ob5kTd9H3S4GOG2nVXyQhOu9O8nBgP555XxWPkJI0tR0JeRilfyTp8WtPdIJHLXBmHMSdEq5+KMxiYABeScsIQ==
less@^4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/less/-/less-4.1.3.tgz#175be9ddcbf9b250173e0a00b4d6920a5b770246"
integrity sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==
dependencies:
copy-anything "^2.0.1"
parse-node-version "^1.0.1"
tslib "^2.3.0"
optionalDependencies:
errno "^0.1.1"
graceful-fs "^4.1.2"
image-size "~0.5.0"
make-dir "^2.1.0"
mime "^1.4.1"
needle "^3.1.0"
source-map "~0.6.0"
lilconfig@^2.0.5, lilconfig@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4"
@ -560,6 +757,23 @@ magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.8"
make-dir@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
dependencies:
pify "^4.0.1"
semver "^5.6.0"
md5@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==
dependencies:
charenc "0.0.2"
crypt "0.0.2"
is-buffer "~1.1.6"
merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@ -573,6 +787,30 @@ micromatch@^4.0.4, micromatch@^4.0.5:
braces "^3.0.2"
picomatch "^2.3.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime@^1.4.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
minimatch@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.2.tgz#0939d7d6f0898acbd1508abe534d1929368a8fff"
integrity sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.6:
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
@ -583,11 +821,30 @@ mitt@^3.0.0:
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd"
integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@^3.3.4:
version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
needle@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/needle/-/needle-3.2.0.tgz#07d240ebcabfd65c76c03afae7f6defe6469df44"
integrity sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==
dependencies:
debug "^3.2.6"
iconv-lite "^0.6.3"
sax "^1.2.4"
nested-property@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/nested-property/-/nested-property-4.0.0.tgz#a67b5a31991e701e03cdbaa6453bc5b1011bb88d"
integrity sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==
node-releases@^2.0.6:
version "2.0.8"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae"
@ -608,11 +865,21 @@ object-hash@^3.0.0:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
parse-node-version@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-posix@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f"
integrity sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@ -628,6 +895,11 @@ pify@^2.3.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
pify@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
pinia@^2.0.13:
version "2.0.28"
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.0.28.tgz#887c982d854972042d9bdfd5bc4fad3b9d6ab02a"
@ -689,6 +961,21 @@ postcss@^8.1.10, postcss@^8.4.12, postcss@^8.4.13, postcss@^8.4.18:
picocolors "^1.0.0"
source-map-js "^1.0.2"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==
querystringify@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@ -713,6 +1000,11 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
resolve@^1.1.7, resolve@^1.22.0, resolve@^1.22.1:
version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
@ -741,12 +1033,32 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
"safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sax@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
seamless-scroll-polyfill@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/seamless-scroll-polyfill/-/seamless-scroll-polyfill-2.2.0.tgz#422f4a7d1b3a7d0ea86f8afad3d6abea2c66a000"
integrity sha512-c4KHfltYY8oFRt987Kl9i4xRLylIpg1YHWsOyH1kj4SP+W+8PtOiKrLyv8L6xbHvwQpofOZ98AWpGPEJK++udQ==
semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
source-map@^0.6.1:
source-map@0.6.1, source-map@^0.6.1, source-map@~0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@ -756,6 +1068,11 @@ sourcemap-codec@^1.4.8:
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
strnum@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
@ -797,6 +1114,11 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
tslib@^2.3.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
typescript@^4.6.2:
version "4.9.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
@ -810,11 +1132,29 @@ update-browserslist-db@^1.0.9:
escalade "^3.1.1"
picocolors "^1.0.0"
url-join@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"
integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==
url-parse@^1.5.3:
version "1.5.10"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
validator@^13.7.0:
version "13.7.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857"
integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==
vite@^2.9.2:
version "2.9.15"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.15.tgz#2858dd5b2be26aa394a283e62324281892546f0b"
@ -832,6 +1172,16 @@ vue-demi@*:
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99"
integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==
vue-i18n@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.2.2.tgz#aeb49d9424923c77e0d6441e3f21dafcecd0e666"
integrity sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==
dependencies:
"@intlify/core-base" "9.2.2"
"@intlify/shared" "9.2.2"
"@intlify/vue-devtools" "9.2.2"
"@vue/devtools-api" "^6.2.1"
vue-router@4:
version "4.1.6"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.1.6.tgz#b70303737e12b4814578d21d68d21618469375a1"

Loading…
Cancel
Save