mirror of https://github.com/veypi/OneAuth.git
update
parent
8f2bcc1591
commit
7990fbcf10
@ -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"),
|
||||||
|
}
|
||||||
|
}
|
@ -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?)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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,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,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,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,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>
|
@ -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,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,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 base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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;
|
transition: all 0.2s linear;
|
||||||
--base-color: #000;
|
|
||||||
--base-bg: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[theme=dark] {
|
.div-btn:hover {
|
||||||
--base-color: #fff;
|
/*border-bottom: #03a9f4 1px solid;*/
|
||||||
--base-bg: #000;
|
}
|
||||||
|
|
||||||
|
.div-btn:hover hr {
|
||||||
|
visibility: visible;
|
||||||
|
width: 80%;
|
||||||
|
right: 10%;
|
||||||
}
|
}
|
||||||
|
@ -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()
|
@ -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 }
|
@ -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>
|
<template>
|
||||||
|
<div></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
</script>
|
</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>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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>
|
</script>
|
||||||
|
|
||||||
|
<style scoped></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>
|
Loading…
Reference in New Issue