文件编辑 管理 上传

master
veypi 1 year ago
parent 24937738db
commit e48c269357

@ -7,11 +7,12 @@
//
use actix_multipart::form::{tempfile::TempFile, MultipartForm};
use actix_web::{post, web, Responder};
use proc::access_read;
use tracing::{info, warn};
use crate::{AppState, Error, Result};
use crate::{models::Token, AppState, Error, Result};
#[derive(Debug, MultipartForm)]
struct UploadForm {
@ -22,27 +23,28 @@ struct UploadForm {
#[access_read("app")]
async fn save_files(
MultipartForm(form): MultipartForm<UploadForm>,
t: web::ReqData<Token>,
stat: web::Data<AppState>,
) -> Result<impl Responder> {
let l = form.files.len();
let t = t.into_inner();
let mut res: Vec<String> = Vec::new();
info!("!|||||||||||_{}_|", l);
for f in form.files {
info!("saving to {:#?}", f);
let fname = f.file_name.unwrap();
let path = format!("{}tmp/{}", stat.media_path, fname);
form.files.into_iter().for_each(|v| {
let fname = v.file_name.unwrap_or("unknown".to_string());
let path = format!("{}tmp/{}.{}", stat.media_path, t.id, fname);
info!("saving to {path}");
match f.file.persist(path) {
Ok(t) => {
info!("{:#?}", t);
res.push(format!("/media/tmp/{}", fname))
match v.file.persist(path) {
Ok(p) => {
info!("{:#?}", p);
res.push(format!("/media/tmp/{}.{}", t.id, fname))
}
Err(e) => {
warn!("{}", e);
return Err(Error::InternalServerError);
// return Err(Error::InternalServerError);
}
};
}
});
Ok(web::Json(res))
}

@ -162,6 +162,12 @@ impl From<aes_gcm::Error> for Error {
}
}
impl From<actix_multipart::MultipartError> for Error {
fn from(e: actix_multipart::MultipartError) -> Self {
Error::BusinessException(format!("{:?}", e))
}
}
impl From<Box<dyn std::fmt::Display>> for Error {
fn from(e: Box<dyn std::fmt::Display>) -> Self {
Error::BusinessException(format!("{}", e))

@ -25,6 +25,7 @@
"mitt": "^3.0.1",
"pinia": "^2.0.11",
"quasar": "^2.6.0",
"vite-plugin-rewrite-all": "^1.0.1",
"vue": "^3.0.0",
"vue-i18n": "^9.2.2",
"vue-router": "^4.0.0",

@ -20,18 +20,18 @@
**示例**
```
[!!#ff0000 红色超链接!!](http://www.qq.com)
[!!#ffffff !!!#000000 黑底白字超链接!!!!!](http://www.qq.com)
[新窗口打开](http://www.qq.com){target=_blank}
[!!#ff0000 红色超链接!!](http://www.google.com)
[!!#ffffff !!!#000000 黑底白字超链接!!!!!](http://www.google.com)
[新窗口打开](http://www.google.com){target=_blank}
鞋子 !32 特大号!
大头 ^`儿子`^ 和小头 ^^`爸爸`^^
爱在~~西元前~~**当下**
```
**效果**
[!!#ff0000 红色超链接!!](http://www.qq.com)
[!!#ffffff !!!#000000 黑底白字超链接!!!!!](http://www.qq.com)
[新窗口打开](http://www.qq.com){target=_blank}
[!!#ff0000 红色超链接!!](http://www.google.com)
[!!#ffffff !!!#000000 黑底白字超链接!!!!!](http://www.google.com)
[新窗口打开](http://www.google.com){target=_blank}
鞋子 !32 特大号!
大头 ^`儿子`^ 和小头 ^^`爸爸`^^
爱在~~西元前~~**当下**
@ -59,21 +59,21 @@
**示例**
```
这是 [腾讯网](https://www.qq.com) 的链接。
这是 [Google](https://www.google.com) 的链接。
这是 [一个引用的][引用一个链接] 的链接。
这是一个包含中文的链接<https://www.qq.com?param=中文>,中文
直接识别成链接https://www.qq.com?param=中文,中文 用空格结束
这是一个包含中文的链接<https://www.google.com?param=中文>,中文
直接识别成链接https://www.google.com?param=中文,中文 用空格结束
[引用一个链接]
[引用一个链接]: https://www.qq.com
[引用一个链接]: https://www.google.com
```
**效果**
这是 [腾讯网](https://www.qq.com) 的链接。
这是 [Google](https://www.google.com) 的链接。
这是 [一个引用的][引用一个链接] 的链接。
这是一个包含中文的链接<https://www.qq.com?param=中文>,中文
直接识别成链接https://www.qq.com?param=中文,中文 用空格结束
这是一个包含中文的链接<https://www.google.com?param=中文>,中文
直接识别成链接https://www.google.com?param=中文,中文 用空格结束
[引用一个链接]
[引用一个链接]: https://www.qq.com
[引用一个链接]: https://www.google.com
---
@ -217,33 +217,33 @@
**示例**
```
标准图片 ![一条dog#100px](images/demo-dog.png)
设置图片大小(相对大小&绝对大小) ![一条dog#10%#50px](images/demo-dog.png)
标准图片 ![一条dog#100px](/cherry/images/demo-dog.png)
设置图片大小(相对大小&绝对大小) ![一条dog#10%#50px](/cherry/images/demo-dog.png)
设置图片对齐方式:
**左对齐+边框**
![一条dog#auto#100px#left#border](images/demo-dog.png)
![一条dog#auto#100px#left#border](/cherry/images/demo-dog.png)
**居中+边框+阴影**
![一条dog#auto#100px#center#B#shadow](images/demo-dog.png)
![一条dog#auto#100px#center#B#shadow](/cherry/images/demo-dog.png)
**右对齐+边框+阴影+圆角**
![一条dog#auto#100px#right#B#S#radius](images/demo-dog.png)
![一条dog#auto#100px#right#B#S#radius](/cherry/images/demo-dog.png)
**浮动左对齐+边框+阴影+圆角**
![一条dog#auto#100px#float-left#B#S#R](images/demo-dog.png)
![一条dog#auto#100px#float-left#B#S#R](/cherry/images/demo-dog.png)
开心也是一天,不开心也是一天
这样就过了两天,汪
```
**效果**
标准图片 ![一条dog#100px](images/demo-dog.png)
设置图片大小(相对大小&绝对大小) ![一条dog#10%#50px](images/demo-dog.png)
标准图片 ![一条dog#100px](/cherry/images/demo-dog.png)
设置图片大小(相对大小&绝对大小) ![一条dog#10%#50px](/cherry/images/demo-dog.png)
设置图片对齐方式:
**左对齐+边框**
![一条dog#auto#100px#left#border](images/demo-dog.png)
![一条dog#auto#100px#left#border](/cherry/images/demo-dog.png)
**居中+边框+阴影**
![一条dog#auto#100px#center#B#shadow](images/demo-dog.png)
![一条dog#auto#100px#center#B#shadow](/cherry/images/demo-dog.png)
**右对齐+边框+阴影+圆角**
![一条dog#auto#100px#right#B#S#radius](images/demo-dog.png)
![一条dog#auto#100px#right#B#S#radius](/cherry/images/demo-dog.png)
**浮动左对齐+边框+阴影+圆角**
![一条dog#auto#100px#float-left#B#S#R](images/demo-dog.png)
![一条dog#auto#100px#float-left#B#S#R](/cherry/images/demo-dog.png)
开心也是一天,不开心也是一天
这样就过了两天,汪
@ -443,14 +443,14 @@ $$
**示例**
```
这是个演示视频 !video[不带封面演示视频](images/demo.mp4)
这是个演示视频 !video[带封面演示视频](images/demo.mp4){poster=images/demo-dog.png}
这是个演示视频 !video[不带封面演示视频](/cherry/images/demo.mp4)
这是个演示视频 !video[带封面演示视频](/cherry/images/demo.mp4){poster=images/demo-dog.png}
这是个假音频!audio[描述](视频链接地址)
```
**效果**
这是个演示视频 !video[不带封面演示视频](images/demo.mp4)
这是个演示视频 !video[带封面演示视频](images/demo.mp4){poster=images/demo-dog.png}
这是个演示视频 !video[不带封面演示视频](/cherry/images/demo.mp4)
这是个演示视频 !video[带封面演示视频](/cherry/images/demo.mp4){poster=images/demo-dog.png}
这是个假音频!audio[描述](视频链接地址)
@ -679,7 +679,7 @@ title 饼图
## 通过快捷按钮修改字体样式
![bubble menu](images/feature_font.png)
![bubble menu](/cherry/images/feature_font.png)
-----
@ -692,7 +692,7 @@ title 饼图
- 可以拖拽调整预览区域的宽度
![copy and paste](images/feature_copy.gif)
![copy and paste](/cherry/images/feature_copy.gif)
-----
@ -761,24 +761,24 @@ title 饼图
> 其中,`宽度`、`高度`支持绝对像素值比如200px、相对外层容器百分比比如50%
`对齐方式`候选值有左对齐缺省、右对齐right、居中center、悬浮左、右对齐float-left/right
![图片尺寸](images/feature_image_size.png)
![图片尺寸](/cherry/images/feature_image_size.png)
-----
### 特性 2根据表格内容生成图表
![表格图表](images/feature_table_chart.png)
![表格图表](/cherry/images/feature_table_chart.png)
-----
### 特性 3字体颜色、字体大小
![字体样式](images/feature_font.png)
![字体样式](/cherry/images/feature_font.png)
------
## 功能特性
### 特性 1复制Html粘贴成MD语法
![html转md](images/feature_copy.gif)
![html转md](/cherry/images/feature_copy.gif)
#### 使用场景
@ -788,7 +788,7 @@ title 饼图
----
### 特性 2经典换行&常规换行
![br](images/feature_br.gif)
![br](/cherry/images/feature_br.gif)
#### 使用场景
@ -797,17 +797,17 @@ title 饼图
-----
### 特性 3: 多光标编辑
![br](images/feature_cursor.gif)
![br](/cherry/images/feature_cursor.gif)
#### 使用场景
想要批量修改?可以试试多光标编辑(快捷键、搜索多光标选中等功能正在开发中)
### 特性 4图片尺寸
![wysiwyg](images/feature_image_wysiwyg.gif)
![wysiwyg](/cherry/images/feature_image_wysiwyg.gif)
### 特性 5导出
![wysiwyg](images/feature_export.png)
![wysiwyg](/cherry/images/feature_export.png)
-------
@ -817,10 +817,10 @@ title 饼图
> CherryMarkdown会判断用户到底变更了哪个段落做到只渲染变更的段落从而提升修改时的渲染性能
![wysiwyg](images/feature_myers.png)
![wysiwyg](/cherry/images/feature_myers.png)
### 局部更新
> CherryMarkdown利用virtual dom机制实现对预览区域需要变更的内容进行局部更新的功能从而减少了浏览器Dom操作提高了修改时预览内容更新的性能
![wysiwyg](images/feature_vdom.gif)
![wysiwyg](/cherry/images/feature_vdom.gif)

@ -11,6 +11,7 @@
const { configure } = require('quasar/wrappers');
const path = require('path');
const pluginRewriteAll = require('vite-plugin-rewrite-all');
module.exports = configure(function(/* ctx */) {
return {
@ -68,7 +69,7 @@ module.exports = configure(function(/* ctx */) {
node: 'node16'
},
vueRouterMode: 'hash', // available values: 'hash', 'history'
vueRouterMode: 'history', // available values: 'hash', 'history'
// vueRouterBase,
// vueDevtools,
// vueOptionsAPI: false,
@ -93,6 +94,7 @@ module.exports = configure(function(/* ctx */) {
// viteVuePluginOptions: {},
vitePlugins: [
pluginRewriteAll.default(),
['@intlify/vite-plugin-vue-i18n', {
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false,

@ -14,12 +14,13 @@ $q.iconMapFn = (iconName) => {
// your custom approach, the following
// is just an example:
if (iconName.startsWith('app:') === true) {
if (iconName.startsWith('v-') === true) {
// we strip the "app:" part
const name = iconName.substring(4)
const name = iconName.substring(2)
console.log(name)
return {
cls: 'my-app-icon ' + name
icon: 'svguse:#icon-' + name
}
}
}
@ -39,10 +40,10 @@ body,
}
.page-h1 {
font-size: 1.5rem;
line-height: 2rem;
font-size: 2.5rem;
line-height: 2.5rem;
margin-left: 2.5rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
margin-top: 1.5rem;
margin-bottom: 2rem;
}
</style>

File diff suppressed because one or more lines are too long

@ -7,6 +7,7 @@
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import msg from '@veypi/msg'
import util from 'src/libs/util';
// Be careful when using SSR for cross-request state pollution
// due to creating a Singleton instance here;
@ -27,7 +28,7 @@ const proxy = axios.create({
// 请求拦截
const beforeRequest = (config: any) => {
// 设置 token
const token = localStorage.getItem('auth_token')
const token = util.getToken()
// NOTE 添加自定义头部
token && (config.headers.auth_token = token)
// config.headers['auth_token'] = ''

@ -14,15 +14,24 @@ import '../assets/icon.js'
import '@veypi/oaer/dist/index.css'
import 'cherry-markdown/dist/cherry-markdown.css';
import oafs from 'src/libs/oafs'
import { Cfg } from '@veypi/oaer'
import util from 'src/libs/util.js'
import evt from 'src/libs/evt.js'
conf.timeout = 5000
oafs.setCfg(util.getToken())
Cfg.token.value = util.getToken()
conf.timeout = 5000
Cfg.host.value = 'http://' + window.location.host
Cfg.token.value = localStorage.getItem('auth_token') || ''
Cfg.uuid.value = 'FR9P5t8debxc11aFF'
evt.on('token', (t) => {
oafs.setCfg(util.getToken())
Cfg.token.value = util.getToken()
})
// "async" is optional;
// more info on params: https://v2.quasar.dev/quasar-cli/boot-files
export default boot(async (/* { app, router, ... } */) => {

@ -1,8 +1,8 @@
<template>
<q-item v-ripple clickable tag="a" :href="link" :to="to">
<q-item-section v-if="icon" avatar>
<q-icon :name="icon" />
</q-item-section>
<q-item class="flex items-center" v-ripple clickable tag="a" :href="link" :to="to">
<!-- <q-item-section v-if="icon" avatar> -->
<!-- </q-item-section> -->
<q-icon size="1.5rem" class="mr-2" :name="icon" />
<q-item-section>
<q-item-label>{{ title }}</q-item-label>

@ -0,0 +1,67 @@
<!--
* FsTree.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-06 15:35
* Distributed under terms of the MIT license.
-->
<template>
<div>
<div :style="{ paddingLeft: depth * 2 + 'rem' }"
class="cursor-pointer rounded-full h-8 pr-4 flex items-center hover:bg-gray-100" @click="toggle">
<q-icon class="transition-all mx-2" :class="[expand ? 'rotate-90' :
'']" style="font-size: 24px;" :name="root.type ===
'directory' ? 'v-caret-right' : 'v-file'"> </q-icon>
<div>
{{ root.filename }}
</div>
<div class="grow"></div>
<div>{{ new Date(root.lastmod).toLocaleString() }}</div>
</div>
<div v-if="expand">
<template v-for="(s, si) of subs" :key="si">
<FsTree :root="s" :depth="depth + 1"></FsTree>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import FsTree from './FsTree.vue'
import { ref } from 'vue';
import oafs, { fileProps } from 'src/libs/oafs';
import { util } from 'src/libs';
let expand = ref(false)
let subs = ref([] as fileProps[])
let props = withDefaults(defineProps<{
root: fileProps,
depth?: number,
}>(),
{
depth: 0
}
)
const toggle = () => {
if (props.root.type === 'file') {
util.goto('/file' + props.root.filename)
return
}
if (!expand.value) {
oafs.dav().dir(props.root.filename).then(
(e: any) => {
subs.value = e
expand.value = true
})
return
}
expand.value = !expand.value
}
</script>
<style scoped></style>

@ -5,13 +5,17 @@
* Distributed under terms of the MIT license.
-->
<template>
<div :id="eid"></div>
<div class="w-full h-full relative">
<!-- <div class="absolute bg-red-400 left-0 top-0 w-full h-full"></div> -->
<div class="w-full h-full" :id="eid"></div>
</div>
</template>
<script lang="ts" setup>
import Cherry from 'cherry-markdown';
import options from './options'
import { onMounted } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { CherryOptions } from 'cherry-markdown/types/cherry';
let editor = {} as Cherry;
let emits = defineEmits<{
@ -20,19 +24,50 @@ let emits = defineEmits<{
let props = withDefaults(defineProps<{
eid?: string,
content?: string
preview?: boolean
}>(),
{
eid: 'v-editor',
content: ''
content: '',
preview: false,
}
)
watch(computed(() => props.preview), (e) => {
if (e) {
let des = editor.getValue()
console.log(des)
emits('updated', des)
}
set_mode(e)
})
watch(computed(() => props.content), (e) => {
if (e) {
editor.setValue(e)
}
})
const set_mode = (preview: boolean) => {
editor.switchModel(preview ? 'previewOnly' : 'edit&preview')
}
const init = () => {
let config = {
value: props.content,
id: props.eid,
// isPreviewOnly: props.preview,
callback: {},
} as CherryOptions;
config.callback.afterInit = () => {
}
editor = new Cherry(Object.assign({}, options, config));
set_mode(props.preview)
}
onMounted(() => {
let config = Object.assign({}, options, {
value: props.content, id:
props.eid
});
editor = new Cherry(config);
init()
})
</script>
@ -41,5 +76,19 @@ iframe.cherry-dialog-iframe {
width: 100%;
height: 100%;
}
.cherry {
background: none;
box-shadow: none;
}
.cherry-previewer {
background: none;
border: none;
}
.cherry-toolbar {
box-shadow: none;
}
</style>

@ -9,16 +9,71 @@
import { CherryOptions } from 'cherry-markdown/types/cherry';
const basicConfig: CherryOptions = {
id: '',
value: '',
externals: {
// echarts: window.echarts,
// katex: window.katex,
// MathJax: window.MathJax,
},
/** 预览区域跟随编辑器光标自动滚动 */
autoScrollByCursor: true,
forceAppend: false,
locale: 'zh_CN',
previewer: {
dom: false,
className: 'cherry-markdown',
// Whether to enable the editing ability of preview area (currently supports editing picture size and table content)
enablePreviewerBubble: true,
// 配置图片懒加载的逻辑
lazyLoadImg: {
// 加载图片时如果需要展示loaing图则配置loading图的地址
loadingImgPath: '',
// 同一时间最多有几个图片请求最大同时加载6张图片
maxNumPerTime: 1,
// 不进行懒加载处理的图片数量如果为0即所有图片都进行懒加载处理 如果设置为-1则所有图片都不进行懒加载处理
noLoadImgNum: 0,
// 首次自动加载几张图片不论图片是否滚动到视野内autoLoadImgNum = -1 表示会自动加载完所有图片
autoLoadImgNum: 3,
// 针对加载失败的图片 或 beforeLoadOneImgCallback 返回false 的图片最多尝试加载几次为了防止死循环最多5次。以图片的src为纬度统计重试次数
maxTryTimesPerSrc: 1,
// 加载一张图片之前的回调函数函数return false 会终止加载操作
beforeLoadOneImgCallback: (img: HTMLImageElement) => true,
// 加载一张图片失败之后的回调函数
failLoadOneImgCallback: (img: HTMLImageElement) => { },
// 加载一张图片之后的回调函数,如果图片加载失败,则不会回调该函数
afterLoadOneImgCallback: (img: HTMLImageElement) => { },
// 加载完所有图片后调用的回调函数
afterLoadAllImgCallback: () => { },
}
},
theme: [],
callback: {
afterChange: () => { },
/** 编辑器完成初次渲染后触发 */
afterInit: () => { },
/** img 标签挂载前触发,可用于懒加载等场景 */
beforeImageMounted: (srcProp: string, src: string) => {
return { srcProp: srcProp, src: src }
},
onClickPreview: () => { },
onCopyCode: (e: ClipboardEvent, code: string) => code,
changeString2Pinyin: (s) => s,
},
isPreviewOnly: false,
fileUpload: (f) => { console.log('uploading file' + f.name) },
fileTypeLimitMap: {
video: "",
audio: "",
image: "",
word: "",
pdf: "",
file: "",
},
openai: false,
engine: {
global: {
urlProcessor(url, srcType) {
console.log(`url-processor`, url, srcType);
// console.log(`url-processor`, url, srcType);
return url;
},
},
@ -57,12 +112,14 @@ const basicConfig: CherryOptions = {
},
},
toolbars: {
showToolbar: true,
theme: 'light',
toolbar: [
'bold',
'italic',
{
strikethrough: ['strikethrough', 'underline', 'sub', 'sup', 'ruby'],
},
// {
// strikethrough: ['strikethrough', 'underline', 'sub', 'sup', 'ruby'],
// },
'size',
'|',
'color',
@ -85,10 +142,10 @@ const basicConfig: CherryOptions = {
'togglePreview',
'export',
],
toolbarRight: [],
// toolbarRight: [],
bubble: ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', 'ruby', '|', 'size', 'color'], // array or false
sidebar: false,
float: false
// sidebar: false,
// float: false
},
drawioIframeUrl: '/cherry/drawio.html',
editor: {

@ -1,14 +1,13 @@
<template>
<div @click="click">
<input enctype="multipart/form-data" ref="file" name="files" multiple type="file" hidden @change="upload">
<input enctype="multipart/form-data" ref="file" name="files" :multiple="multiple" type="file" hidden @change="upload">
<slot></slot>
</div>
</template>
<script lang="ts" setup>
// import { createClient } from "webdav";
import { ref } from 'vue';
import axios from "axios";
import oafs from 'src/libs/oafs';
let file = ref<HTMLInputElement>()
let emits = defineEmits<{
@ -17,8 +16,10 @@ let emits = defineEmits<{
}>()
let props = withDefaults(defineProps<{
multiple?: boolean,
renames?: string
}>(), {
multiple: false
multiple: false,
renames: ''
})
function click() {
@ -27,61 +28,14 @@ function click() {
const upload = (evt: Event) => {
evt.preventDefault()
let f = (evt.target as HTMLInputElement).files as any
var data = new FormData();
console.log(f)
for (let i of f) {
console.log(i)
data.append('files', i, i.data)
}
axios.post("/api/upload/", data, {
headers: {
"Content-Type": 'multipart/form-data',
'auth_token': localStorage.getItem('auth_token')
}
}).then(e => {
console.log(e.data)
emits('success', props.multiple ? e.data : e.data[0])
let f = (evt.target as HTMLInputElement).files as FileList
oafs.upload(f, props.renames?.split(/[, ]+/)).then((e: any) => {
console.log(e)
emits('success', props.multiple ? e : e[0])
})
// var token = sessionStorage.getItem('token')
// const config = {
// headers: {
// 'Content-Type': 'multipart/form-data'
// }
// }
// window.API.post('https://110.10.56.10:8000/images/?token=' + token, data, config)
// .then(response => this.$router.push('/listImage'))
// .catch((error) => {
// console.log(JSON.stringify(error))
// })
}
// async function dav_upload() {
// let prefix = '/file/public/app/' + app.id + '/'
// let client = createClient(prefix,
// { headers: { auth_token: localStorage.getItem('auth_token') || '' } })
// 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>

@ -50,25 +50,25 @@ const Links = ref([
{
title: '应用中心',
caption: '',
icon: 'apps',
icon: 'v-apps',
to: { name: 'home' }
},
{
title: '',
caption: '',
icon: 'home',
icon: 'v-home',
to: { name: 'app.home', params: { id: id.value } }
},
{
title: '用户管理',
caption: 'oa.veypi.com',
icon: 'people',
icon: 'v-team',
to: { name: 'app.user', params: { id: id.value } }
},
{
title: '应用设置',
caption: '',
icon: 'settings',
icon: 'v-setting',
to: { name: 'app.settings', params: { id: id.value } }
},
] as MenuLink[])

@ -1,10 +1,10 @@
<template>
<q-layout view="hHh LpR fFf">
<q-header elevated class="bg-primary text-white" height-hint="98">
<q-toolbar class="h-16 pl-0">
<q-toolbar class="pl-0">
<q-toolbar-title class="flex items-center cursor-pointer" @click="router.push({ name: 'home' })">
<q-icon size="3.5rem" color="aqua" name='svguse:#icon-glassdoor' style="color: aqua;"></q-icon>
<q-icon size="3rem" class="mx-1" color="aqua" name='v-glassdoor' style="color: aqua;"></q-icon>
<q-separator dark vertical inset />
<span class="ml-3">
统一认证系统
@ -12,31 +12,31 @@
</q-toolbar-title>
<q-icon class="mx-2" size="2rem" @click="$q.fullscreen.toggle()"
:name="$q.fullscreen.isActive ? 'fullscreen_exit' : 'fullscreen'" />
:name="$q.fullscreen.isActive ? 'v-compress' : 'v-expend'" />
<q-icon class="mx-2" size="1.5rem" @click="$q.dark.toggle"
:name="$q.dark.mode ? 'light_mode' : 'dark_mode'"></q-icon>
<OAer v-if="user.ready" @logout="user.logout" :is-dark="$q.dark.mode as boolean"></OAer>
<q-icon class="mx-2" size="2rem" @click="$q.dark.toggle" :name="$q.dark.mode ? 'v-light' : 'v-dark'"></q-icon>
<OAer v-if="user.ready" @logout="user.logout" :is-dark="$q.dark.mode as boolean">
123
</OAer>
</q-toolbar>
<q-toolbar class="">
<q-icon @click="toggleLeftDrawer" class="cursor-pointer" name="menu" size="sm"></q-icon>
<q-tabs align="left">
<!-- <q-route-tab to="/page1" label="Page One" /> -->
<!-- <q-route-tab to="/page2" label="Page Two" /> -->
<!-- <q-route-tab to="/page3" label="Page Three" /> -->
</q-tabs>
</q-toolbar>
<!-- <q-toolbar class=""> -->
<!-- <q-icon @click="toggleLeftDrawer" class="cursor-pointer" name="menu" size="sm"></q-icon> -->
<!-- <q-tabs align="left"> -->
<!-- <q-route-tab to="/page1" label="Page One" /> -->
<!-- <q-route-tab to="/page2" label="Page Two" /> -->
<!-- <q-route-tab to="/page3" label="Page Three" /> -->
<!-- </q-tabs> -->
<!-- </q-toolbar> -->
</q-header>
<q-drawer show-if-above :mini="miniState" @mouseover="miniState = false" @mouseout="miniState = true" mini-to-overlay
:width="200" :breakpoint="500" bordered v-model="leftDrawerOpen" side="left" class="pt-4">
<q-drawer show-if-above :mini="miniState" :width="140" :breakpoint="500" bordered side="left" class="pt-4">
<Menu></Menu>
</q-drawer>
<q-page-container class="flex">
<q-page class="w-full">
<router-view v-slot="{ Component }">
<transition mode="out-in" enter-active-class="animate__fadeInLeft" leave-active-class="animate__fadeOutRight">
<transition mode="out-in" enter-active-class="animate__fadeIn" leave-active-class="animate__fadeOut">
<component class="animate__animated animate__400ms" :is="Component"></component>
</transition>
</router-view>
@ -55,27 +55,23 @@
<script setup lang="ts">
import { ref } from 'vue';
import { util } from 'src/libs';
import { useRouter } from 'vue-router';
import Menu from 'src/components/menu.vue'
import { useAppStore } from 'src/stores/app';
import { useUserStore } from 'src/stores/user';
import { OAer } from "@veypi/oaer";
const app = useAppStore()
const user = useUserStore()
const router = useRouter()
const leftDrawerOpen = ref(false)
const miniState = ref(true)
const miniState = ref(false)
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value
miniState.value = !miniState.value
}
</script>

@ -0,0 +1,12 @@
/*
* evt.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-08 02:21
* Distributed under terms of the MIT license.
*/
import mitt from "mitt";
const evt = mitt()
export default evt

@ -0,0 +1,93 @@
/*
* fs.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-08 01:55
* Distributed under terms of the MIT license.
*/
import axios from "axios";
import { Base64 } from 'js-base64'
import util from "./util";
import { createClient, WebDAVClient } from 'webdav'
export interface fileProps {
filename: string,
basename: string,
lastmod: string,
size: number,
type: "directory" | "file",
etag: string
}
let cfg = {
token: '',
host: '',
dav: {} as WebDAVClient,
}
const setCfg = (token: string) => {
cfg.token = token
cfg.dav = createClient('/file/',
{ headers: { auth_token: cfg.token } })
}
const rename = (o: string, n?: string) => {
let ext = '.' + o.split('.').pop()?.toLowerCase()
if (n) {
return n + ext
}
let d = new Date().getTime()
return d + Base64.encode(o) + ext
}
const get = (url: string): Promise<string> => {
return fetch(url, { headers: { auth_token: util.getToken() } }).then((response) => response.text())
}
const upload = (f: FileList | File[], renames?: string[]) => {
return new Promise<string[]>((resolve, reject) => {
var data = new FormData();
for (let i = 0; i < f.length; i++) {
let nf = renames ? new File([f[i]], rename(f[i].name, renames[i]), { type: f[i].type }) : f[i]
data.append('files', nf, nf.name)
}
axios.post("/api/upload/", data, {
headers: {
"Content-Type": 'multipart/form-data',
'auth_token': cfg.token,
}
}).then(e => {
resolve(e.data)
}).catch(reject)
})
}
const dav = () => {
return {
stat: cfg.dav.stat,
dir: cfg.dav.getDirectoryContents,
upload: (dir: string, name: string, file: any) => {
return new Promise((resolve, reject) => {
let reader = new FileReader()
reader.onload = function(event) {
var res = event.target?.result
// let data = new Blob([res])
cfg.dav.putFileContents(name, res).then(e => {
resolve(e)
}).catch(reject)
}
reader.readAsArrayBuffer(file)
});
}
}
}
export default {
setCfg,
get,
upload,
dav,
}

@ -1,4 +1,5 @@
import axios from 'axios'
import evt from './evt'
function padLeftZero(str: string): string {
return ('00' + str).substr(str.length)
@ -38,7 +39,11 @@ const util = {
name + '=' + escape(value) + ';expires=' + exp.toLocaleString()
},
getToken() {
return localStorage.auth_token
return localStorage.getItem('auth_token') || ''
},
setToken(t: string) {
evt.emit('token', t)
localStorage.setItem('auth_token', t)
},
addTokenOf(url: string) {
return url + '?auth_token=' + encodeURIComponent(this.getToken())

@ -9,6 +9,17 @@ import { RouteLocationRaw } from 'vue-router';
export { type Auths, type modelsSimpleAuth, NewAuths, R } from './auth'
export interface DocItem {
name: string
url: string
version?: string
}
export interface DocGroup {
name: string
icon: string
items?: DocItem[]
}
export interface MenuLink {
title: string;

@ -1,7 +1,7 @@
<template>
<div class="flex justify-center items-center w-full h-full">
<div class="text-center text-xl">
<q-icon style="font-size: 200px" name="svguse:#icon-404"></q-icon>
<q-icon style="font-size: 200px" name="v-404"></q-icon>
<div>
路径失效啦! {{ count }}
</div>

@ -12,16 +12,17 @@
''
}" round icon="save_as" class="" />
</q-page-sticky>
<Editor v-if="app.id" :eid="app.id + '.des'" :content="app.des" @updated="save"></Editor>
<Editor v-if="app.id" :eid="app.id + '.des'" :preview="!edit_mode" :content="content" @updated="save"></Editor>
</div>
</template>
<script lang="ts" setup>
import { inject, onMounted, ref, Ref } from 'vue';
import { computed, inject, onMounted, ref, Ref, watch } from 'vue';
import { modelsApp } from 'src/models';
import api from 'src/boot/api';
import msg from '@veypi/msg';
import Editor from 'src/components/editor'
import oafs from 'src/libs/oafs';
@ -29,11 +30,23 @@ import Editor from 'src/components/editor'
let edit_mode = ref(false)
let app = inject('app') as Ref<modelsApp>
let content = ref()
watch(computed(() => app.value.id), () => {
if (app.value.des) {
oafs.get(app.value.des).then(e => content.value = e)
}
})
const save = (des: string) => {
api.app.update(app.value.id, { des: des }).then(e => {
edit_mode.value = false
app.value.des = des as string
let a = new File([des], app.value.name + '.md');
oafs.upload([a]).then(url => {
api.app.update(app.value.id, { des: url[0] }).then(e => {
edit_mode.value = false
app.value.des = url[0]
}).catch(e => {
msg.Warn("更新失败: " + e)
})
}).catch(e => {
msg.Warn("更新失败: " + e)
})
@ -41,12 +54,7 @@ const save = (des: string) => {
const sync_editor = () => {
if (edit_mode.value) {
// console.log(editor.getHtml())
// let des = editor.getValue()
// return
}
edit_mode.value = true
edit_mode.value = !edit_mode.value
}

@ -0,0 +1,68 @@
<!--
* doc.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-07 22:07
* Distributed under terms of the MIT license.
-->
<template>
<div>
<h1 class="page-h1">文档中心</h1>
<div class="mx-8 mt-10">
<template v-for="(doc, i) in Docs" :key="i">
<div class="mb-10">
<div class="text-xl flex items-center mb-4">
<q-icon class="mx-2" :name="doc.icon"></q-icon>
<span>{{ doc.name }}</span>
</div>
<div class="flex gap-8">
<template v-for="item in doc.items" :key="item.name">
<q-chip class="" clickable outline @click="$router.push({
name: 'doc_item',
params: { url: item.url, typ: 'public' }
})" icon="bookmark" color="none">
<span>{{ item.name }}</span>
</q-chip>
</template>
</div>
<q-separator class="mt-6" inset></q-separator>
</div>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { DocGroup } from 'src/models';
import { ref } from 'vue';
const Docs = ref<DocGroup[]>([
{
name: '用户',
icon: 'v-team',
items: [
{ name: '用户注册授权过程', url: '' },
{ name: '用户角色与权限', url: '' },
{ name: 'api文档', url: '' }
]
},
{
name: "应用",
icon: 'v-apps',
items: [
{ name: '应用创建及基本设置', url: '' },
{ name: '应用权限设置', url: '' },
{ name: '应用对接oa流程', url: '' },
],
},
{
name: "系统使用",
icon: "v-setting",
items: [
{ name: "编辑器使用及语法", url: 'markdown.md' }
]
}
])
</script>
<style scoped></style>

@ -0,0 +1,64 @@
<!--
* docItem.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-07 22:16
* Distributed under terms of the MIT license.
-->
<template>
<div class="w-full h-full">
<h1 class="page-h1">文档中心</h1>
<div>
{{ url }}
</div>
<q-inner-loading :showing="!visible" label="Please wait..." label-class="text-teal" label-style="font-size: 1.1em" />
<Editor v-if='doc' eid='doc' preview :content="doc"></Editor>
</div>
</template>
<script lang="ts" setup>
import msg from '@veypi/msg';
import Editor from 'src/components/editor'
import oafs from 'src/libs/oafs';
import { computed, watch, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
let doc = ref('')
const visible = ref(false)
let route = useRoute()
let router = useRouter()
let url = computed(() => {
if (route.params.typ === 'public') {
return '/doc/' + route.params.url
}
return route.params.url
})
watch(url, u => {
render(u as string)
})
const render = (url: string) => {
console.log(url)
if (!url) {
return
}
oafs.get(url).then((value) => {
doc.value = value
visible.value = true
}).catch(e => {
console.warn(e)
msg.Warn('访问文档地址不存在')
router.back()
});
}
onMounted(() => {
render(url.value as string)
})
</script>
<style scoped></style>

@ -0,0 +1,31 @@
<!--
* fsFile.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-08 05:12
* Distributed under terms of the MIT license.
-->
<template>
<div>
<h1 class="page-h1">云文件中心</h1>
<div class="px-4">
<FsTree v-if="root.filename" :root="root"></FsTree>
</div>
</div>
</template>
<script lang="ts" setup>
import FsTree from 'src/components/FsTree.vue';
import oafs, { fileProps } from 'src/libs/oafs';
import { onMounted, ref } from 'vue';
let root = ref({} as fileProps)
onMounted(() => {
oafs.dav().stat('/').then(e => {
console.log(e)
root.value = e as fileProps
})
})
</script>
<style scoped></style>

@ -54,7 +54,7 @@ const onSubmit = () => {
console.log(data.value)
api.user.login(data.value.username,
data.value.password).then((data: any) => {
localStorage.auth_token = data.auth_token
util.setToken(data.auth_token)
msg.Info('登录成功')
user.fetchUserData()
let url = route.query.redirect || data.redirect || '/'

@ -0,0 +1,19 @@
<!--
* settings.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-08 06:10
* Distributed under terms of the MIT license.
-->
<template>
<div>
<h1 class="page-h1">系统设置</h1>
<div class="px-4">
</div>
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped></style>

@ -0,0 +1,17 @@
<!--
* user.vue
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-08 05:31
* Distributed under terms of the MIT license.
-->
<template>
<div>
<h1 class="page-h1">账号设置</h1>
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped></style>

@ -33,11 +33,13 @@ const routes: RouteRecordRaw[] = [
redirect: 'home',
children: [
loadcomponents('home', 'home', 'IndexPage'),
loadcomponents('user', 'user', '404'),
loadcomponents('file', 'file', '404'),
loadcomponents('settings', 'settings', '404'),
loadcomponents('user', 'user', 'user'),
loadcomponents('fs', 'fs', 'fs'),
loadcomponents('doc', 'doc', 'doc'),
loadcomponents('doc/:typ/:url(.*)', 'doc_item', 'docItem'),
loadcomponents('settings', 'settings', 'settings'),
{
path: 'app/:id?',
path: 'app/:id',
component: () => import("../layouts/AppLayout.vue"),
redirect: { name: 'app.home' },
children: [

@ -13,19 +13,29 @@ const defaultLinks: MenuLink[] = [
{
title: '应用中心',
caption: '',
icon: 'apps',
icon: 'v-apps',
to: { name: 'home' }
},
{
title: '文件管理',
caption: '',
icon: 'v-folder',
to: { name: 'fs' }
},
{
title: '账号设置',
caption: 'oa.veypi.com',
icon: 'person',
icon: 'v-user',
to: { name: 'user' }
},
{
title: '文档中心',
icon: 'v-file-exception',
to: { name: 'doc' }
},
{
title: '设置',
caption: '',
icon: 'settings',
icon: 'v-setting',
to: { name: 'settings' }
},
]

@ -11,6 +11,7 @@ import { Auths, modelsUser, NewAuths } from 'src/models';
import { Base64 } from 'js-base64'
import router from 'src/router';
import api from 'src/boot/api';
import util from 'src/libs/util';
export const useUserStore = defineStore('user', {
state: () => ({
@ -24,11 +25,11 @@ export const useUserStore = defineStore('user', {
actions: {
logout() {
this.ready = false
localStorage.removeItem('auth_token')
util.setToken('')
router.push({ name: 'login' })
},
fetchUserData() {
let token = localStorage.getItem('auth_token')?.split('.');
let token = util.getToken().split('.');
if (!token || token.length !== 3) {
return false
}

@ -1193,6 +1193,11 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
connect-history-api-fallback@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==
content-disposition@0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
@ -4221,6 +4226,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==
vite-plugin-rewrite-all@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vite-plugin-rewrite-all/-/vite-plugin-rewrite-all-1.0.1.tgz#ee711a3d114634abb922a0e50e56736d7e9a324a"
integrity sha512-W0DAchC8ynuQH0lYLIu5/5+JGfYlUTRD8GGNtHFXRJX4FzzB9MajtqHBp26zq/ly9sDt5BqrfdT08rv3RbB0LQ==
dependencies:
connect-history-api-fallback "^1.6.0"
vite@^2.9.13:
version "2.9.16"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.16.tgz#daf7ba50f5cc37a7bf51b118ba06bc36e97898e9"

Loading…
Cancel
Save