update app home

master
veypi 1 year ago
parent 5ae9c6eb50
commit 36d55d1f1f

@ -2,6 +2,15 @@
name = "oab"
version = "0.1.0"
edition = "2021"
default-run = "web"
[[bin]]
name = "web"
path = "src/main.rs"
[[bin]]
name = "test"
path = "src/test.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

@ -5,8 +5,8 @@
// Distributed under terms of the Apache license.
//
//
use actix_web::{delete, get, post, web, Responder};
use proc::{access_create, access_read};
use actix_web::{delete, get, patch, post, put, web, Responder};
use proc::{access_create, access_delete, access_read, access_update};
use sea_orm::{ActiveModelTrait, EntityTrait, TransactionTrait};
use serde::{Deserialize, Serialize};
use tracing::info;
@ -54,12 +54,6 @@ pub struct App {
#[access_read("app")]
pub async fn list(stat: web::Data<AppState>) -> Result<impl Responder> {
let res = app::Entity::find().all(stat.db()).await?;
// let result = sqlx::query_as::<_,app::Model>(
// "select app.*,app_userstatus as status from app left join app_user on app_user.user_id = ? && app_user.app_id = app.id",
// ).bind(_auth_token.id)
// .fetch_all(stat.sqlx())
// .await?;
Ok(web::Json(res))
}
@ -101,12 +95,46 @@ pub async fn create(
level: sea_orm::ActiveValue::Set(AccessLevel::ALL as i32),
..Default::default()
};
let ac: access::Model = ac.insert(&db).await?;
ac.insert(&db).await?;
libs::user::connect_to_app(t.id.clone(), obj.id.clone(), &db, Some(obj.clone())).await?;
db.commit().await?;
Ok(web::Json(obj))
}
#[derive(Debug, Deserialize, Serialize)]
pub struct UpdateOpt {
name: Option<String>,
icon: Option<String>,
enable_register: Option<String>,
des: Option<String>,
host: Option<String>,
redirect: Option<String>,
}
#[patch("/app/{id}")]
#[access_update("app")]
pub async fn update(
id: web::Path<String>,
stat: web::Data<AppState>,
data: web::Json<UpdateOpt>,
) -> Result<impl Responder> {
let data = data.into_inner();
let id = id.into_inner();
let obj = app::Entity::find_by_id(&id).one(stat.db()).await?;
let mut obj: app::ActiveModel = match obj {
Some(o) => o.into(),
None => return Err(Error::NotFound(id)),
};
if let Some(name) = data.name {
obj.name = sea_orm::Set(name)
};
let obj = obj.update(stat.db()).await?;
Ok(web::Json(obj))
}
#[delete("/app/{id}")]
#[access_delete("app")]
pub async fn del(_id: web::Path<String>) -> Result<impl Responder> {
Ok("")
}

@ -5,11 +5,12 @@
// Distributed under terms of the MIT license.
//
use actix_web::{get, web, Responder};
use actix_web::{get, post, web, Responder};
use proc::access_read;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, TransactionTrait};
use crate::{
libs,
models::{app, app_user},
AppState, Error, Result,
};
@ -51,3 +52,16 @@ pub async fn get(
Ok(web::Json(res))
}
}
#[post("/app/{aid}/user/{uid}")]
#[access_read("app")]
pub async fn add(
params: web::Path<(String, String)>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
let (aid, uid) = params.into_inner();
let db = stat.db().begin().await?;
let res = libs::user::connect_to_app(uid, aid, &db, None).await?;
db.commit().await?;
Ok(web::Json(res))
}

@ -27,5 +27,5 @@ pub fn routes(cfg: &mut web::ServiceConfig) {
.service(app::create)
.service(app::del);
cfg.service(appuser::get);
cfg.service(appuser::get).service(appuser::add);
}

@ -0,0 +1,6 @@
//
// app.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-10-06 19:39
// Distributed under terms of the MIT license.
//

@ -9,3 +9,4 @@ pub mod auth;
pub mod cors;
pub mod fs;
pub mod user;
pub mod app;

@ -9,7 +9,9 @@ use crate::{
models::{self, app, app_user, user_role},
Error, Result,
};
use sea_orm::{ActiveModelTrait, ConnectionTrait, DatabaseTransaction, EntityTrait};
use sea_orm::{
ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseTransaction, EntityTrait, QueryFilter,
};
// 尝试绑定应用
pub async fn connect_to_app(
@ -18,6 +20,15 @@ pub async fn connect_to_app(
db: &DatabaseTransaction,
app_obj: Option<app::Model>,
) -> Result<app_user::Model> {
match app_user::Entity::find()
.filter(app_user::Column::AppId.eq(&aid))
.filter(app_user::Column::UserId.eq(&uid))
.one(db)
.await?
{
Some(au) => return Ok(au),
None => {}
};
let app_obj = match app_obj {
Some(o) => o,
None => match app::Entity::find_by_id(&aid).one(db).await? {

@ -168,7 +168,6 @@ impl From<Box<dyn std::fmt::Display>> for Error {
}
}
impl actix_web::Responder for Error {
type Body = actix_web::body::BoxBody;
fn respond_to(self, _req: &actix_web::HttpRequest) -> HttpResponse<Self::Body> {
@ -182,7 +181,7 @@ impl error::ResponseError for Error {
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.insert_header(("error", self.to_string()))
.body("".to_string())
.body(self.to_string())
}
fn status_code(&self) -> StatusCode {

@ -0,0 +1,8 @@
//
// test.rs
// Copyright (C) 2023 veypi <i@veypi.com>
// 2023-10-06 22:45
// Distributed under terms of the MIT license.
//
fn main() {}

@ -14,15 +14,16 @@
},
"dependencies": {
"@quasar/extras": "^1.16.4",
"@veypi/msg": "^0.1.0",
"@veypi/one-icon": "2",
"@veypi/msg": "^0.1.1",
"@veypi/oaer": "^0.0.1",
"@veypi/one-icon": "2",
"animate.css": "^4.1.1",
"axios": "^1.2.1",
"js-base64": "^3.7.5",
"mitt": "^3.0.1",
"pinia": "^2.0.11",
"quasar": "^2.6.0",
"vditor": "^3.9.6",
"vue": "^3.0.0",
"vue-i18n": "^9.2.2",
"vue-router": "^4.0.0",

@ -155,6 +155,7 @@ module.exports = configure(function(/* ctx */) {
plugins: [
'LoadingBar',
'AppFullscreen',
'Dialog'
]
},

@ -37,7 +37,7 @@ export default {
list(id: string) {
return ajax.get(this.local + id)
},
add(uid: number) {
add(uid: string) {
return ajax.post(this.local + uid)
},
update(uid: number, status: string) {

@ -9,11 +9,15 @@
import { boot } from 'quasar/wrappers'
import '@veypi/msg/index.css'
import { conf } from '@veypi/msg'
import '../assets/icon.js'
import '@veypi/oaer/dist/index.css'
import { Cfg } from '@veypi/oaer'
import 'vditor/dist/index.css';
conf.timeout = 5000
Cfg.host.value = 'http://' + window.location.host
Cfg.token.value = localStorage.getItem('auth_token') || ''
Cfg.uuid.value = 'FR9P5t8debxc11aFF'

@ -17,6 +17,7 @@
</template>
<script setup lang="ts">
import msg from "@veypi/msg";
import { useQuasar } from "quasar";
import api from "src/boot/api";
import { AUStatus, modelsApp, modelsAppUser } from "src/models";
import { useUserStore } from "src/stores/user";
@ -27,16 +28,30 @@ const router = useRouter()
let props = withDefaults(defineProps<{
core: modelsApp
core: modelsApp,
is_part: boolean
}>(),
{}
)
const $q = useQuasar()
const u = useUserStore()
function Go() {
switch (props.core.au.status) {
if (props.is_part) {
router.push({ name: "app.home", params: { id: props.core.id } });
return
}
// $q.dialog({
// title: '',
// message: ' ' + props.core.name,
// cancel: true,
// }).onOk(() => {
api.app.user(props.core.id).add(u.id).then(e => {
switch (e.status) {
case AUStatus.OK:
msg.Info('加入成功')
router.push({ name: "app.home", params: { id: props.core.id } });
return;
case AUStatus.Applying:
@ -49,28 +64,10 @@ function Go() {
msg.Warn("已被禁止使用");
return;
}
// api.app.user(props.core.id).add(useUserStore().id).then(e => {
// console.log(e)
// })
// api.app
// .user(props.core.UUID)
// .add(store.state.user.id)
// .Start(
// (e) => {
// bar.finish();
// if (e.Status === "ok") {
// router.push({ name: "app.main", params: { uuid: props.core.UUID } });
// return;
// }
// props.core.UserStatus = e.Status;
// msg.info("");
// },
// (e) => {
// msg.warning(": " + e);
// bar.error();
// }
// );
// return;
}).catch(e => {
msg.Warn("加入失败" + e)
})
}
</script>
<style scoped>

@ -5,8 +5,13 @@
* Distributed under terms of the MIT license.
-->
<template>
<div>
<h1>{{ app.name }}</h1>
<div class="p-4">
<div class="flex items-center">
<q-avatar class="mx-2" round size="4rem">
<img :src="app.icon">
</q-avatar>
<h1 class="text-4xl">{{ app.name }}</h1>
</div>
<router-view :data="{ a: 1 }" />
</div>
</template>
@ -14,10 +19,11 @@
<script lang="ts" setup>
import msg from '@veypi/msg';
import api from 'src/boot/api';
import { modelsApp } from 'src/models';
import { MenuLink, modelsApp } from 'src/models';
import { useMenuStore } from 'src/stores/menu';
import { computed, watch, ref, onMounted, provide, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import { RouteLocationNamedRaw } from 'vue-router';
let route = useRoute();
let menu = useMenuStore()
@ -29,19 +35,52 @@ provide('app', app)
const sync_app = () => {
api.app.get(id.value as string).then((e: modelsApp) => {
app.value = e
Links.value[1].title = e.name
for (let i in Links.value) {
let l: RouteLocationNamedRaw = Links.value[i].to as any
if (l.params) {
l.params.id = e.id
}
}
}).catch(e => {
msg.Warn('sync app data failed: ' + e)
})
}
watch(id, () => {
const Links = ref([
{
title: '应用中心',
caption: '',
icon: 'apps',
to: { name: 'home' }
},
{
title: '',
caption: '',
icon: 'home',
to: { name: 'app.home', params: { id: id.value } }
},
{
title: '用户管理',
caption: 'oa.veypi.com',
icon: 'people',
to: { name: 'app.user', params: { id: id.value } }
},
{
title: '应用设置',
caption: '',
icon: 'settings',
to: { name: 'app.settings', params: { id: id.value } }
},
] as MenuLink[])
watch(id, (e) => {
if (e) {
sync_app()
}
})
onMounted(() => {
sync_app()
menu.set([
])
menu.set(Links.value)
})
onBeforeUnmount(() => {
menu.load_default()

@ -16,7 +16,7 @@
<q-icon class="mx-2" size="1.5rem" @click="$q.dark.toggle"
:name="$q.dark.mode ? 'light_mode' : 'dark_mode'"></q-icon>
<OAer @logout="user.logout" :is-dark="$q.dark.mode as boolean"></OAer>
<OAer v-if="user.ready" @logout="user.logout" :is-dark="$q.dark.mode as boolean"></OAer>
</q-toolbar>
<!-- <q-toolbar class=""> -->
<!-- <q-icon @click="toggleLeftDrawer" class="cursor-pointer" name="menu" size="sm"></q-icon> -->
@ -35,16 +35,20 @@
<q-page-container class="flex">
<q-page class="w-full">
<router-view />
<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"></component>
</transition>
</router-view>
</q-page>
</q-page-container>
<q-footer bordered class="bg-grey-8 text-white flex justify-around">
<span class="hover:text-black cursor-pointer" @click="$router.push({ name: 'about' })">关于OA</span>
<span class="hover:text-black cursor-pointer">使用须知</span>
<span class="hover:text-black cursor-pointer" @click="util.goto('https://veypi.com')">
©2021 veypi
</span>
</q-footer>
<!-- <q-footer bordered class="bg-grey-8 text-white flex justify-around"> -->
<!-- <span class="hover:text-black cursor-pointer" @click="$router.push({ name: 'about' })">关于OA</span> -->
<!-- <span class="hover:text-black cursor-pointer">使用须知</span> -->
<!-- <span class="hover:text-black cursor-pointer" @click="util.goto('https://veypi.com')"> -->
<!-- ©2021 veypi -->
<!-- </span> -->
<!-- </q-footer> -->
</q-layout>
</template>
@ -74,3 +78,9 @@ function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value
}
</script>
<style scoped>
.animate__400ms {
--animate-duration: 300ms;
}
</style>

@ -6,17 +6,57 @@
-->
<template>
<div>
<q-page-sticky position="top-right" :offset="[27, 27]">
<q-btn @click="sync_editor" :style="{
color: edit_mode ? 'red' :
''
}" round icon="save_as" class="" />
</q-page-sticky>
<div v-if="edit_mode" id="vditor"></div>
<div v-else>
{{ app }}
</div>
</div>
</template>
<script lang="ts" setup>
import { inject } from 'vue';
import { inject, onMounted, ref } from 'vue';
import { modelsApp } from 'src/models';
import Vditor from 'vditor';
import api from 'src/boot/api';
let edit_mode = ref(false)
const vditor = ref<Vditor | null>(null);
let app = inject('app') as modelsApp
const sync_editor = () => {
if (edit_mode.value) {
api.app.update(app.id, { des: "" }).then(e => {
edit_mode.value = false
console.log(e)
})
return
}
edit_mode.value = true
setTimeout(() => {
vditor.value = new Vditor('vditor', {
toolbarConfig: {
hide: true
},
after: () => {
// vditor.value is a instance of Vditor now and thus can be safely used here
vditor.value!.setValue('Vue Composition API + Vditor + TypeScript Minimal Example');
},
});
}, 0)
}
onMounted(() => {
})
</script>
<style scoped></style>

@ -0,0 +1,17 @@
<!--
* AppUser.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-06 20:44
* Distributed under terms of the MIT license.
-->
<template>
<div>
app.user
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped></style>

@ -8,9 +8,9 @@
</q-btn>
</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 class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
<div v-for="(item, k) in ofApps" class="flex items-center justify-center" :key="k">
<AppCard :core="item"></AppCard>
<AppCard :core="item" :is_part="true"></AppCard>
</div>
</div>
</div>
@ -18,7 +18,7 @@
<h1 class="page-h1">应用中心</h1>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
<div v-for="(item, k) in apps" class="flex items-center justify-center" :key="k">
<AppCard :core="item"></AppCard>
<AppCard :core="item" :is_part="false"></AppCard>
</div>
</div>
</div>
@ -74,11 +74,10 @@ function getApps() {
api.app.list().then(
(e: modelsApp[]) => {
apps.value = e;
api.app.user('-').list(user.id).then((aus: modelsAppUser[]) => {
api.app.user('-').list(user.id).then((aus: modelsApp[]) => {
for (let i in aus) {
let ai = apps.value.findIndex(a => a.id === aus[i].app_id)
let ai = apps.value.findIndex(a => a.id === aus[i].id)
if (ai >= 0) {
apps.value[ai].au = aus[i]
if (aus[i].status === AUStatus.OK) {
ofApps.value.push(apps.value[ai])
apps.value.splice(ai, 1)

@ -19,7 +19,7 @@ import routes from './routes';
* with the Router instance.
*/
export default route(function(/* { store, ssrContext } */) {
function newRouter(/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
@ -33,7 +33,6 @@ export default route(function(/* { store, ssrContext } */) {
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.VUE_ROUTER_BASE),
});
const u = useUserStore()
Router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !util.checkLogin()) {
@ -46,6 +45,7 @@ export default route(function(/* { store, ssrContext } */) {
}
}
if (to.meta.checkAuth) {
const u = useUserStore()
if (!to.meta.checkAuth(u.auth, to)) {
// if (window.$msg) {
@ -56,4 +56,6 @@ export default route(function(/* { store, ssrContext } */) {
}
})
return Router;
});
};
export default newRouter();

@ -34,14 +34,16 @@ const routes: RouteRecordRaw[] = [
children: [
loadcomponents('home', 'home', 'IndexPage'),
loadcomponents('user', 'user', '404'),
loadcomponents('file', 'file', '404'),
loadcomponents('settings', 'settings', '404'),
{
path: 'app/:id?',
component: () => import("../layouts/AppLayout.vue"),
redirect: { name: 'app.home' },
children: [
loadcomponents('home', 'app.home', 'IndexPage'),
loadcomponents('user', 'app.user', 'AppHome'),
loadcomponents('home', 'app.home', 'AppHome'),
loadcomponents('user', 'app.user', 'AppUser'),
loadcomponents('settings', 'app.settings', 'IndexPage'),
]
}
],

@ -17,7 +17,7 @@ const defaultLinks: MenuLink[] = [
to: { name: 'home' }
},
{
title: '用户管理',
title: '账号设置',
caption: 'oa.veypi.com',
icon: 'person',
to: { name: 'user' }

@ -8,8 +8,8 @@
import { defineStore } from 'pinia';
import { Auths, modelsUser, NewAuths } from 'src/models';
import { useRouter } from 'vue-router';
import { Base64 } from 'js-base64'
import router from 'src/router';
import api from 'src/boot/api';
export const useUserStore = defineStore('user', {
@ -23,10 +23,9 @@ export const useUserStore = defineStore('user', {
},
actions: {
logout() {
// this.ready = false
// localStorage.removeItem('auth_token')
// const r = useRouter()
// r.push({ name: 'login' })
this.ready = false
localStorage.removeItem('auth_token')
router.push({ name: 'login' })
},
fetchUserData() {
let token = localStorage.getItem('auth_token')?.split('.');

@ -465,6 +465,11 @@
resolved "https://registry.yarnpkg.com/@veypi/msg/-/msg-0.1.0.tgz#2ebe899527a11ed11f68c2c96f468cfcc66ad3d4"
integrity sha512-58dj5nnpHsxaiK5sbPiDK5t8OF4uvN+kAmWhU0BRAgXHpkxkZNZ8rn7hXvSVybG1BbM8EuMNkq0lIxGYNKl8aw==
"@veypi/msg@^0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@veypi/msg/-/msg-0.1.1.tgz#94864ae2c0a81991b8a30d87f12d2245fdebbead"
integrity sha512-UiAF/Y0EGT/37tGApptzHBNUpo78LbnrEkCqGAGMkJp86wrUyOgTAvuvQ197Ifqw9PIbjZM9dAgMv4DfMJQEYA==
"@veypi/oaer@^0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@veypi/oaer/-/oaer-0.0.1.tgz#b22ebaf72a7bfd5abf62f099b72b533a8cf27abd"
@ -1207,6 +1212,11 @@ didyoumean@^1.2.2:
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
diff-match-patch@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@ -3469,6 +3479,13 @@ vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
vditor@^3.9.6:
version "3.9.6"
resolved "https://registry.yarnpkg.com/vditor/-/vditor-3.9.6.tgz#c6a9e460984992d00b7d4b1f9f79bc75cbba4e34"
integrity sha512-97sPNHnBpfEFnk5WARCpmdKxgUiPtp0/fPLAUmzZ+axFFf7kExWHRNIUO7OTQzEUMJP/rcXESQTlYGhgKYrsOQ==
dependencies:
diff-match-patch "^1.0.5"
vite@^2.9.13:
version "2.9.16"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.16.tgz#daf7ba50f5cc37a7bf51b118ba06bc36e97898e9"

Loading…
Cancel
Save