feat: fsupload

v3
veypi 4 weeks ago
parent 9900b6987c
commit fee81d6bee

@ -4,7 +4,7 @@
// Distributed under terms of the MIT license. // Distributed under terms of the MIT license.
// //
import axios, { AxiosError, type AxiosResponse } from 'axios'; import axios, { AxiosError, AxiosInstance, type AxiosResponse } from 'axios';
// Be careful when using SSR for cross-request state pollution // Be careful when using SSR for cross-request state pollution
// due to creating a Singleton instance here; // due to creating a Singleton instance here;
@ -13,15 +13,6 @@ import axios, { AxiosError, type AxiosResponse } from 'axios';
// "export default () => {}" function below (which runs individually // "export default () => {}" function below (which runs individually
// for each client) // for each client)
axios.defaults.withCredentials = true
const proxy = axios.create({
withCredentials: true,
baseURL: "/api/",
headers: {
'content-type': 'application/json;charset=UTF-8',
},
});
export const token = { export const token = {
value: '', value: '',
update: () => { update: () => {
@ -64,79 +55,89 @@ const beforeRequest = (config: any) => {
// config.headers['auth_token'] = '' // config.headers['auth_token'] = ''
return config return config
} }
proxy.interceptors.request.use(beforeRequest)
// 响应拦截器 // 响应拦截器
const responseSuccess = (response: AxiosResponse) => { const responseSuccess = (client: AxiosInstance) => {
// eslint-disable-next-line yoda return (response: AxiosResponse) => {
// 这里没有必要进行判断axios 内部已经判断 // eslint-disable-next-line yoda
// const isOk = 200 <= response.status && response.status < 300 // 这里没有必要进行判断axios 内部已经判断
let data = response.data // const isOk = 200 <= response.status && response.status < 300
if (typeof data === 'object') { let data = response.data
if (data.code !== 0) { if (typeof data === 'object') {
return responseFailed({ response } as any) if (data.code !== 0) {
return responseFailed(client)({ response } as any)
}
data = data.data
} }
data = data.data return Promise.resolve(data)
} }
return Promise.resolve(data)
} }
const responseFailed = (error: AxiosError) => { const responseFailed = (client: AxiosInstance) => {
const { response } = error return (error: AxiosError) => {
const config = response?.config const { response } = error
const data = response?.data || {} as any const config = response?.config
if (!window.navigator.onLine) { const data = response?.data || {} as any
alert('没有网络') if (!window.navigator.onLine) {
return Promise.reject(new Error('请检查网络连接')) alert('没有网络')
} return Promise.reject(new Error('请检查网络连接'))
}
let needRetry = true let needRetry = true
if (response?.status == 404) { if (response?.status == 404) {
needRetry = false needRetry = false
} else if (response?.status == 401) { } else if (response?.status == 401) {
needRetry = false needRetry = false
// AuthNotFound = New(40100, "auth not found") // AuthNotFound = New(40100, "auth not found")
// AuthExpired = New(40102, "auth expired") // AuthExpired = New(40102, "auth expired")
if (data.code === 40102 || data.code === 40100) { if (data.code === 40102 || data.code === 40100) {
token.value = '' token.value = ''
return token.update().then(() => { return token.update().then(() => {
return requestRetry(1000, response!) return requestRetry(client)(1000, response!)
}) })
}
} else if (response?.status == 500) {
needRetry = false
} }
if (!needRetry) {
return Promise.reject(data || response)
};
return requestRetry(client)(1000, response!)
} }
if (!needRetry) {
return Promise.reject(data || response)
};
return requestRetry(1000, response!)
} }
const requestRetry = (delay = 0, response: AxiosResponse) => { const requestRetry = (client: AxiosInstance) => {
const config = response?.config return (delay = 0, response: AxiosResponse) => {
// @ts-ignore const config = response?.config
const { __retryCount = 0, retryDelay = 1000, retryTimes } = config; // @ts-ignore
// 在请求对象上设置重试次数 const { __retryCount = 0, retryDelay = 1000, retryTimes } = config;
// @ts-ignore // 在请求对象上设置重试次数
config.__retryCount = __retryCount + 1; // @ts-ignore
// 判断是否超过了重试次数 config.__retryCount = __retryCount + 1;
if (__retryCount >= retryTimes) { // 判断是否超过了重试次数
return Promise.reject(response?.data || response?.headers.error) if (__retryCount >= retryTimes) {
} return Promise.reject(response?.data || response?.headers.error)
if (delay <= 0) { }
return proxy.request(config as any) if (delay <= 0) {
return client.request(config as any)
}
// 延时处理
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, delay)
}).then(() => {
return client.request(config as any)
})
} }
// 延时处理
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, delay)
}).then(() => {
return proxy.request(config as any)
})
} }
proxy.interceptors.response.use(responseSuccess, responseFailed) const create_client = (client: AxiosInstance) => {
client.interceptors.request.use(beforeRequest)
client.interceptors.response.use(responseSuccess(client), responseFailed(client))
return client
}
interface data { interface data {
json?: any json?: any
query?: any query?: any
@ -144,6 +145,7 @@ interface data {
header?: any header?: any
} }
function transData(d: data) { function transData(d: data) {
let opts = { params: d.query, data: {}, headers: {} as any } let opts = { params: d.query, data: {}, headers: {} as any }
if (d.form) { if (d.form) {
@ -161,24 +163,31 @@ function transData(d: data) {
} }
export const webapi = { export const webapi = {
client: create_client(axios.create({
withCredentials: true,
baseURL: "/api/",
headers: {
'content-type': 'application/json;charset=UTF-8',
},
})),
Get<T>(url: string, req: data): Promise<T> { Get<T>(url: string, req: data): Promise<T> {
return proxy.request<T, any>(Object.assign({ method: 'get', url: url }, transData(req))) return webapi.client.request<T, any>(Object.assign({ method: 'get', url: url }, transData(req)))
}, },
Head<T>(url: string, req: data): Promise<T> { Head<T>(url: string, req: data): Promise<T> {
return proxy.request<T, any>(Object.assign({ method: 'head', url: url }, transData(req))) return webapi.client.request<T, any>(Object.assign({ method: 'head', url: url }, transData(req)))
}, },
Delete<T>(url: string, req: data): Promise<T> { Delete<T>(url: string, req: data): Promise<T> {
return proxy.request<T, any>(Object.assign({ method: 'delete', url: url }, transData(req))) return webapi.client.request<T, any>(Object.assign({ method: 'delete', url: url }, transData(req)))
}, },
Post<T>(url: string, req: data): Promise<T> { Post<T>(url: string, req: data): Promise<T> {
return proxy.request<T, any>(Object.assign({ method: 'post', url: url }, transData(req))) return webapi.client.request<T, any>(Object.assign({ method: 'post', url: url }, transData(req)))
}, },
Put<T>(url: string, req: data): Promise<T> { Put<T>(url: string, req: data): Promise<T> {
return proxy.request<T, any>(Object.assign({ method: 'put', url: url }, transData(req))) return webapi.client.request<T, any>(Object.assign({ method: 'put', url: url }, transData(req)))
}, },
Patch<T>(url: string, req: data): Promise<T> { Patch<T>(url: string, req: data): Promise<T> {
return proxy.request<T, any>(Object.assign({ method: 'patch', url: url }, transData(req))) return webapi.client.request<T, any>(Object.assign({ method: 'patch', url: url }, transData(req)))
}, },
} }

@ -22,7 +22,7 @@ export default class {
let frame_user = v({ let frame_user = v({
class: 'voa-on', class: 'voa-on',
vclass: [() => logic.ready ? 'voa-scale-in' : 'voa-scale-off'], vclass: [() => logic.ready ? 'voa-scale-in' : 'voa-scale-off'],
children: [() => `<img src="${logic.user.icon}" />`], children: [() => `<img src="${logic.user.icon || logic.token.refresh.icon}" />`],
onclick: () => { onclick: () => {
logic.getDetailUser() logic.getDetailUser()
if (!this.slide) { if (!this.slide) {
@ -42,7 +42,6 @@ export default class {
} }
}) })
return () => { return () => {
console.log('gen_avatar', logic.ready)
if (logic.ready) { if (logic.ready) {
return frame_user return frame_user
} else { } else {

@ -0,0 +1,177 @@
/*
* fs.ts
* Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-08 01:55
* Distributed under terms of the MIT license.
*/
import * as webdav from 'webdav'
import { proxy } from "./v2dom";
import logic from "./logic";
export interface fileProps {
filename: string,
basename: string,
lastmod: string,
size: number,
type: "directory" | "file",
etag: string
}
function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class davWraper {
private client: webdav.WebDAVClient
private host: string
private prefix: string
private token: string
constructor(host: string, prefix: string, token: string) {
this.host = host
this.prefix = prefix
this.token = token
this.client = webdav.createClient(host + prefix)
}
set(k: 'host' | 'token', value: string) {
if (value !== this[k]) {
this[k] = value
if (k === 'token') {
this.client.setHeaders({
authorization: "bearer " + value
})
} else if (k === 'host') {
this.client = webdav.createClient(this.host + this.prefix)
}
}
}
putFileContents(filename: string, data: string, options?: webdav.PutFileContentsOptions) {
return this.retry(() => this.client.putFileContents(filename, data, options))
}
getFileContents(filename: string, options?: webdav.GetFileContentsOptions) {
return this.retry(() => this.client.getFileContents(filename, options))
}
getDirectoryContents(path: string, options?: webdav.GetDirectoryContentsOptions) {
return this.retry(() => this.client.getDirectoryContents(path, options))
}
stat(path: string, options?: webdav.StatOptions) {
return this.retry(() => this.client.stat(path, options))
}
private retry<T>(fn: () => Promise<T>): Promise<T> {
let retries = 0;
function attempt(): Promise<T> {
return fn().catch(error => {
if (retries < 3) {
retries++;
console.log(`Attempt ${retries} failed, retrying after 1 second...`);
return delay(1000).then(attempt);
} else {
// 超过最大重试次数后不再重试
throw error;
}
})
}
return attempt()
}
}
let token = logic.token.oa.raw()
const user = new davWraper(logic.Host(), '/fs/u/', token)
const app = new davWraper(logic.Host(), '/fs/a/', token)
export const set_host = (h: string) => {
user.set('host', h)
app.set('host', h)
}
const sync = () => {
if (logic.token.oa.isVaild()) {
let t = logic.token.oa.raw()
// console.warn('sync oafs token: ' + t)
user.set('token', t)
app.set('token', t)
}
}
proxy.Listen(() => {
sync()
})
const rename = (o: string, n?: string) => {
let ext = '.' + o.split('.').pop()?.toLowerCase()
if (n) {
return n + ext
}
let d = new Date().getTime()
return d + o + ext
}
// const get = (url: string): Promise<string> => {
// return fetch(cfg.Host() + url, { headers: { authorization: "bearer " + cfg.oa_token.value } }).then((response) => response.text())
// }
// rename 可以保持url不变
// const upload = (f: FileList | File[], dir?: string, renames?: string[]) => {
// return new Promise<string[]>((resolve, reject) => {
// var data = new FormData();
// for (let i = 0; i < f.length; i++) {
// let nf = new File([f[i]], rename(f[i].name, renames && renames[i] ? renames[i] : undefined), { type: f[i].type })
// data.append('files', nf, nf.name)
// }
// axios.post("/api/upload/" + (dir || ''), data, {
// headers: {
// "Content-Type": 'multipart/form-data',
// 'auth_token': cfg.oa_token.value,
// }
// }).then(e => {
// resolve(e.data)
// }).catch(reject)
// })
// }
const get_dav = (client: webdav.WebDAVClient, base_url: string) => {
return {
client: client,
stat: client.stat,
dir: client.getDirectoryContents,
uploadstr: (dir: string, name: string, data: string) => {
if (dir.startsWith('/')) {
dir = dir.slice(1)
}
return new Promise((resolve, reject) => {
let temp = () => {
let reader = new FileReader()
reader.onload = function (event) {
var res = event.target?.result
// let data = new Blob([res])
client.putFileContents(dir + name, res).then(e => {
if (e) {
resolve(base_url + dir + name)
}
}).catch(reject)
}
reader.readAsArrayBuffer(new Blob([data], { type: 'plain/text' }))
}
client.stat(dir).then(() => {
temp()
}).catch((_) => {
client.createDirectory(dir, { recursive: true }).then(() => {
temp()
}).catch(e => {
console.warn(e)
})
})
});
}
}
}
export default {
user,
app,
rename,
}

@ -149,9 +149,9 @@ const logic = proxy.Watch({
}) })
}) })
}, },
host: '', host: window.location.origin,
Host() { Host() {
return this.host || (window.location.protocol + window.location.host) return this.host
}, },
goto(url: string) { goto(url: string) {
if (url.startsWith('http')) { if (url.startsWith('http')) {

@ -10,6 +10,7 @@ import bus from './bus'
import logic from './logic' import logic from './logic'
import ui from './components' import ui from './components'
import api from './api' import api from './api'
import fs from './fs'
export default new class { export default new class {
@ -22,6 +23,9 @@ export default new class {
access() { access() {
return logic.token.app.access! return logic.token.app.access!
} }
fs() {
return fs
}
init(host?: string, code?: string) { init(host?: string, code?: string) {
if (host) { if (host) {
logic.host = host logic.host = host

@ -10,7 +10,6 @@ let app = useAppConfig()
let menu = useMenuStore() let menu = useMenuStore()
onMounted(() => { onMounted(() => {
menu.default() menu.default()
console.log('init app')
app.layout.size = [document.body.clientWidth, document.body.clientHeight] app.layout.size = [document.body.clientWidth, document.body.clientHeight]
window.onresize = () => { window.onresize = () => {
app.layout.size = [document.body.clientWidth, document.body.clientHeight] app.layout.size = [document.body.clientWidth, document.body.clientHeight]
@ -19,7 +18,7 @@ onMounted(() => {
</script> </script>
<style > <style>
body, body,
html { html {
height: 100vh; height: 100vh;

@ -95,7 +95,7 @@ $toolbarBtnHoverColor: $toolbarBtnHoverColorLight;
$toolbarColorItemHoverBorderColor: rgb(247, 133, 83); $toolbarColorItemHoverBorderColor: rgb(247, 133, 83);
$sidebarShadow: $shadow; $sidebarShadow: $shadow;
/** 编辑区域样式 */ /** 编辑区域样式 */
$editorBg: rgb(255, 255, 255); $editorBg: rgba(0, 0, 0, 0);
$editorColor: rgb(63, 74, 86); $editorColor: rgb(63, 74, 86);
$editorSelectedBg: $toolbarBtnBgHover; $editorSelectedBg: $toolbarBtnBgHover;
$editorUrlBg: rgb(215, 230, 254); $editorUrlBg: rgb(215, 230, 254);
@ -103,7 +103,7 @@ $editorCursorColor: rgb(0, 0, 0);
$editorImportantColor: rgb(34, 139, 230); $editorImportantColor: rgb(34, 139, 230);
$editorCodeColor: rgb(77, 171, 247); $editorCodeColor: rgb(77, 171, 247);
/** 预览区域样式 */ /** 预览区域样式 */
$previewBg: none; $previewBg: rgba(0, 0, 0, 0);
$previewMobileBgColor: $previewBg; $previewMobileBgColor: $previewBg;
/** markdown样式 */ /** markdown样式 */
$mdColor: $editorColor; $mdColor: $editorColor;
@ -117,6 +117,14 @@ $mdBlockquoteBg: rgb(231, 245, 255);
/** 编辑器样式 */ /** 编辑器样式 */
.cherry.theme__oa { .cherry.theme__oa {
background-color: $editorBg;
box-shadow: $shadow;
.cherry-drag {
background-color: rgba(0, 0, 0, 0.3);
width: 5px;
z-index: 10;
}
/** 顶部按钮, 选中文字时弹出的按钮, 光标focus到空行时联想出的按钮, 侧边栏按钮 */ /** 顶部按钮, 选中文字时弹出的按钮, 光标focus到空行时联想出的按钮, 侧边栏按钮 */
.cherry-toolbar, .cherry-toolbar,
@ -125,6 +133,7 @@ $mdBlockquoteBg: rgb(231, 245, 255);
.cherry-sidebar { .cherry-sidebar {
background: $toolbarBg; background: $toolbarBg;
border-color: $toolbarBg; border-color: $toolbarBg;
box-shadow: none;
.cherry-toolbar-button { .cherry-toolbar-button {
color: $toolbarBtnColor; color: $toolbarBtnColor;
@ -243,6 +252,7 @@ $mdBlockquoteBg: rgb(231, 245, 255);
.cherry-previewer { .cherry-previewer {
background-color: $previewBg; background-color: $previewBg;
border-left: none;
.cherry-mobile-previewer-content { .cherry-mobile-previewer-content {
background-color: $previewMobileBgColor; background-color: $previewMobileBgColor;

@ -1,4 +1,4 @@
<!-- <!--
* appcard.vue * appcard.vue
* Copyright (C) 2024 veypi <i@veypi.com> * Copyright (C) 2024 veypi <i@veypi.com>
* 2024-06-07 16:48 * 2024-06-07 16:48
@ -15,21 +15,22 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { models } from "#imports";
import msg from "@veypi/msg"; import msg from "@veypi/msg";
import oaer from "@veypi/oaer";
const router = useRouter() const router = useRouter()
let props = withDefaults(defineProps<{ let props = withDefaults(defineProps<{
core: modelsApp, core: models.App,
is_part: boolean is_part: boolean
}>(), }>(),
{} {}
) )
const u = useUserStore()
function Go() { function Go() {
if (props.is_part) { if (props.is_part) {
@ -41,7 +42,8 @@ function Go() {
// message: ' ' + props.core.name, // message: ' ' + props.core.name,
// cancel: true, // cancel: true,
// }).onOk(() => { // }).onOk(() => {
api.app.user(props.core.id).add(u.id).then(e => { api.app.AppUserPost(props.core.id, { user_id: oaer.local().id, status: 'ok' }).then(e => {
msg.Info('申请成功')
switch (e.status) { switch (e.status) {
case AUStatus.OK: case AUStatus.OK:
msg.Info('加入成功') msg.Info('加入成功')

@ -1,4 +1,4 @@
<!-- <!--
* index.vue * index.vue
* Copyright (C) 2023 veypi <i@veypi.com> * Copyright (C) 2023 veypi <i@veypi.com>
* 2023-10-07 18:57 * 2023-10-07 18:57
@ -7,7 +7,7 @@
<template> <template>
<div class="w-full h-full"> <div class="w-full h-full">
<!-- <div class="absolute bg-red-400 left-0 top-0 w-full h-full"></div> --> <!-- <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 class="w-full h-full veditor" :id="eid"></div>
</div> </div>
</template> </template>
@ -15,7 +15,7 @@
import 'cherry-markdown/dist/cherry-markdown.css'; import 'cherry-markdown/dist/cherry-markdown.css';
import Cherry from 'cherry-markdown'; import Cherry from 'cherry-markdown';
import options from './options' import options from './options'
import { oafs } from '@veypi/oaer' // import { oafs } from '@veypi/oaer'
let editor = {} as Cherry; let editor = {} as Cherry;
let emits = defineEmits<{ let emits = defineEmits<{
@ -40,6 +40,7 @@ watch(computed(() => props.modelValue), (e) => {
}) })
watch(computed(() => props.content), (e) => { watch(computed(() => props.content), (e) => {
if (e) { if (e) {
console.log(e)
editor.setValue(e) editor.setValue(e)
} }
}) })
@ -59,11 +60,11 @@ const fileUpload = (f: File, cb: (url: string, params: any) => void) => {
* @param params.width 设置宽度可以是像素也可以是百分比图片视频场景下生效 * @param params.width 设置宽度可以是像素也可以是百分比图片视频场景下生效
* @param params.height 设置高度可以是像素也可以是百分比图片视频场景下生效 * @param params.height 设置高度可以是像素也可以是百分比图片视频场景下生效
*/ */
oafs.upload([f], props.static_dir).then((e: any) => { // oafs.upload([f], props.static_dir).then((e: any) => {
cb(e[0], { // cb(e[0], {
name: f.name, isBorder: false, isShadow: false, isRadius: false, width: '', height: '', // name: f.name, isBorder: false, isShadow: false, isRadius: false, width: '', height: '',
}) // })
}) // })
} }
const saveMenu = Cherry.createMenuHook('保存', { const saveMenu = Cherry.createMenuHook('保存', {
@ -104,25 +105,29 @@ onMounted(() => {
}) })
</script> </script>
<style> <style lang="scss">
iframe.cherry-dialog-iframe { .cherry-dialog {
width: 100%; .cherry-dialog--body {
height: 100%; bottom: 45px !important;
}
iframe {
width: 100%;
height: 100%;
}
}
.cherry { .cherry-dialog--foot {
background: none; height: 45px !important;
box-shadow: none; line-height: 34px !important;
height: 100%; }
} }
.cherry-previewer { .veditor .cherry .cherry-previewer {
background: none; // background-color: rgba(0, 0, 0, 0);
border: none; // box-shadow: none;
} }
.cherry-toolbar { .cherry-toolbar {
box-shadow: none; // box-shadow: none;
} }
</style> </style>

@ -6,11 +6,29 @@
*/ */
import { CherryOptions } from 'cherry-markdown/types/cherry'; import { type CherryOptions } from 'cherry-markdown/types/cherry';
import '@/assets/css/editor.scss'
const basicConfig: CherryOptions = { const basicConfig: CherryOptions = {
id: '', id: '',
value: '', value: '',
nameSpace: 'cherry',
themeSettings: {
// 主题列表,用于切换主题
themeList: [
{ className: 'dark', label: '黑' },
{ className: 'light', label: '白' },
{ className: 'orange', label: '橘里橘气' },
],
// 目前应用的主题
mainTheme: 'oa',
/** 代码块主题 */
codeBlockTheme: 'dark',
/** 行内代码主题,只有 red 和 black 两个主题 */
inlineCodeTheme: 'red',
/** 工具栏主题,只有 light 和 dark 两个主题,优先级低于 mainTheme */
toolbarTheme: 'dark',
},
externals: { externals: {
// echarts: window.echarts, // echarts: window.echarts,
// katex: window.katex, // katex: window.katex,
@ -98,23 +116,8 @@ const basicConfig: CherryOptions = {
theme: 'red', theme: 'red',
}, },
codeBlock: { codeBlock: {
theme: 'twilight', // Default to dark theme
wrap: true, // If it exceeds the length, whether to wrap the line. If false, the scroll bar will be displayed wrap: true, // If it exceeds the length, whether to wrap the line. If false, the scroll bar will be displayed
lineNumber: true, // Default display line number lineNumber: true, // Default display line number
customRenderer: {
// Custom syntax renderer
},
/**
* indentedCodeBlock Is the switch whether indent code block is enabled
*
* this syntax is not supported by default in versions before 6.X.
* Because cherry's development team thinks the syntax is too ugly (easy to touch by mistake)
* The development team hopes to completely replace this syntax with ` ` code block syntax
* However, in the subsequent communication, the development team found that the syntax had better display effect in some scenarios
* Therefore, the development team in 6 This syntax was introduced in version X
* if you want to upgrade the following versions of services without users' awareness, you can remove this syntax:
* indentedCodeBlockfalse
*/
indentedCodeBlock: true, indentedCodeBlock: true,
}, },
fontEmphasis: { fontEmphasis: {
@ -134,8 +137,6 @@ const basicConfig: CherryOptions = {
}, },
emoji: { emoji: {
useUnicode: false, useUnicode: false,
// customResourceURL: 'https://github.githubassets.com/images/icons/emoji/unicode/${code}.png?v8',
upperCase: true,
}, },
toc: { toc: {
/** By default, only one directory is rendered */ /** By default, only one directory is rendered */
@ -188,8 +189,8 @@ const basicConfig: CherryOptions = {
'graph', 'graph',
'togglePreview', 'togglePreview',
'export', 'export',
'saveMenu', // @ts-ignore
'backMenu' 'saveMenu', 'backMenu'
], ],
// toolbarRight: [], // toolbarRight: [],
bubble: ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', 'ruby', '|', 'size', 'color'], // array or false bubble: ['bold', 'italic', 'underline', 'strikethrough', 'sub', 'sup', 'quote', 'ruby', '|', 'size', 'color'], // array or false

@ -14,8 +14,9 @@ export interface PatchOpts {
name?: string name?: string
icon?: string icon?: string
des?: string des?: string
participate?: string
init_role_id?: string init_role_id?: string
typ?: string
status?: string
} }
export function Patch(app_id: string, json: PatchOpts) { export function Patch(app_id: string, json: PatchOpts) {
return webapi.Patch<models.App>(`/app/${app_id}`, { json }) return webapi.Patch<models.App>(`/app/${app_id}`, { json })
@ -29,7 +30,8 @@ export interface PostOpts {
name: string name: string
icon: string icon: string
des: string des: string
participate: string typ: string
status: string
} }
export function Post(json: PostOpts) { export function Post(json: PostOpts) {
return webapi.Post<models.App>(`/app`, { json }) return webapi.Post<models.App>(`/app`, { json })

@ -4,8 +4,10 @@ import * as token from "./token"
import * as role from "./role" import * as role from "./role"
import * as app from "./app" import * as app from "./app"
import * as access from "./access" import * as access from "./access"
import { token as apitoken } from './webapi'
export default { export default {
apitoken,
user, user,
token, token,
role, role,

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

@ -1,6 +1,6 @@
// //
// Copyright (C) 2024 veypi <i@veypi.com> // Copyright (C) 2024 veypi <i@veypi.com>
// 2024-10-28 17:43:57 // 2024-10-28 19:22:21
// Distributed under terms of the MIT license. // Distributed under terms of the MIT license.
// //
@ -34,7 +34,7 @@ export const token = {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve(); resolve();
}, 1000) }, 100)
}).then(() => { }).then(() => {
return token.update() return token.update()
}) })
@ -100,9 +100,11 @@ const responseFailed = (error: AxiosError) => {
if (data.code === 40102 || data.code === 40100) { if (data.code === 40102 || data.code === 40100) {
token.value = '' token.value = ''
return token.update().then(() => { return token.update().then(() => {
return requestRetry(1000, response!) return requestRetry(100, response!)
}) })
} }
} else if (response?.status == 500) {
needRetry = false
} }
if (!needRetry) { if (!needRetry) {
return Promise.reject(data || response) return Promise.reject(data || response)

@ -33,17 +33,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import { OneIcon } from '@veypi/one-icon' import { OneIcon } from '@veypi/one-icon'
import oaer from '../../oaer/lib/main' import oaer from '@veypi/oaer'
import '@veypi/oaer/dist/index.css'
let app = useAppConfig() let app = useAppConfig()
let router = useRouter() let router = useRouter()
let user = useUserStore()
app.host = window.location.protocol + '//' + window.location.host app.host = window.location.protocol + '//' + window.location.host
oaer.init(app.host).then(() => { api.apitoken.value = oaer.Token()
oaer.init(app.host).then((e) => {
oaer.render_ui('oaer') oaer.render_ui('oaer')
user.fetchUserData() api.apitoken.set_updator(oaer.TokenRefresh)
}).catch(() => { }).catch(() => {
oaer.logout() oaer.logout()
}) })
@ -80,7 +79,6 @@ const toggle_theme = () => {
} }
onMounted(() => { onMounted(() => {
console.log('mount frame')
oaer.render_ui('oaer') oaer.render_ui('oaer')
}) })

@ -6,6 +6,9 @@ export default defineNuxtConfig({
'~/assets/css/app.scss', '~/assets/css/app.scss',
'~/assets/css/tailwind.css', '~/assets/css/tailwind.css',
], ],
plugins: [
'~/plugins/third'
],
postcss: { postcss: {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},

@ -12,10 +12,10 @@
"dependencies": { "dependencies": {
"@pinia/nuxt": "^0.5.1", "@pinia/nuxt": "^0.5.1",
"@veypi/msg": "^0.2.0", "@veypi/msg": "^0.2.0",
"@veypi/oaer": "^0.2.4", "@veypi/oaer": "^0.3.0",
"@veypi/one-icon": "^3.0.0", "@veypi/one-icon": "^3.0.0",
"axios": "^1.7.2", "axios": "^1.7.2",
"cherry-markdown": "^0.8.42", "cherry-markdown": "^0.8.49",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",

@ -1,4 +1,4 @@
<!-- <!--
* app.[id].vue * app.[id].vue
* Copyright (C) 2024 veypi <i@veypi.com> * Copyright (C) 2024 veypi <i@veypi.com>
* 2024-06-07 17:46 * 2024-06-07 17:46
@ -17,13 +17,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { models } from '#imports';
import msg from '@veypi/msg'; import msg from '@veypi/msg';
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
let menu = useMenuStore() let menu = useMenuStore()
let core = ref({} as modelsApp) let core = ref({} as models.App)
const set_menu = () => { const set_menu = () => {
let p = '/app/' + core.value.id let p = '/app/' + core.value.id
menu.set([ menu.set([
@ -35,7 +36,7 @@ const set_menu = () => {
} }
onMounted(() => { onMounted(() => {
api.app.get(route.params.id as string).then((e) => { api.app.Get(route.params.id as string).then((e) => {
core.value = e core.value = e
set_menu() set_menu()
}).catch(e => { }).catch(e => {
@ -55,4 +56,3 @@ onBeforeRouteLeave(() => {
</script> </script>
<style scoped></style> <style scoped></style>

@ -1,4 +1,4 @@
<!-- <!--
* index.vue * index.vue
* Copyright (C) 2024 veypi <i@veypi.com> * Copyright (C) 2024 veypi <i@veypi.com>
* 2024-06-07 17:46 * 2024-06-07 17:46
@ -7,7 +7,7 @@
<template> <template>
<div> <div>
<div class="vbtn" v-if="preview_mode" @click="preview_mode = false"> <div class="vbtn" v-if="preview_mode" @click="preview_mode = false">
ss <OneIcon name='edit-square'>plus</OneIcon>
</div> </div>
<Editor style="" v-if="core.id" :eid="core.id + '.des'" v-model="preview_mode" :content="content" @save="save"> <Editor style="" v-if="core.id" :eid="core.id + '.des'" v-model="preview_mode" :content="content" @save="save">
</Editor> </Editor>
@ -15,17 +15,19 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { oafs } from '@veypi/oaer' import type { models } from '#imports';
import oaer from '@veypi/oaer'
let props = withDefaults(defineProps<{ let props = withDefaults(defineProps<{
core: modelsApp, core: models.App,
}>(), }>(),
{} {}
) )
let preview_mode = ref(true) let preview_mode = ref(true)
let content = ref() let content = ref('编辑')
watch(computed(() => props.core.id), () => { watch(computed(() => props.core.id), () => {
sync() sync()
@ -33,22 +35,27 @@ watch(computed(() => props.core.id), () => {
const sync = () => { const sync = () => {
if (props.core.des) { if (props.core.des) {
oafs.get(props.core.des).then(e => content.value = e) console.log(props.core.des)
oaer.fs().app.getFileContents("/net/go.mod", { format: 'text' }).then((e) => {
content.value = e as string
})
} }
} }
const save = (des: string) => { const save = (des: string) => {
let a = new File([des], props.core.name + '.md'); oaer.fs().app.putFileContents("/info/des.md", des).then((e) => {
oafs.upload([a], props.core.id).then(url => { console.log(e)
api.app.update(props.core.id, { des: url[0] }).then(e => {
preview_mode.value = true
props.core.des = url[0]
}).catch(e => {
// msg.Warn(": " + e)
})
}).catch(e => {
// msg.Warn(": " + e)
}) })
// oafs.upload([a], props.core.id).then(url => {
// api.app.update(props.core.id, { des: url[0] }).then(e => {
// preview_mode.value = true
// props.core.des = url[0]
// }).catch(e => {
// // msg.Warn(": " + e)
// })
// }).catch(e => {
// // msg.Warn(": " + e)
// })
} }
@ -59,4 +66,3 @@ onMounted(() => {
</script> </script>
<style scoped></style> <style scoped></style>

@ -12,7 +12,7 @@
<div class="flex justify-between"> <div class="flex justify-between">
<h1 class="page-h1">我的应用</h1> <h1 class="page-h1">我的应用</h1>
<div class="my-5 mr-10"> <div class="my-5 mr-10">
<div class='vbtn bg-gray-400' @click="new_flag = true" v-if="user.auth.Get(R.App, '').CanCreate()"> <div class='vbtn bg-gray-400' @click="new_flag = true" v-if="oaer.access().Get('app', '').CanCreate()">
</div> </div>
</div> </div>
</div> </div>
@ -64,28 +64,18 @@
<script setup lang="ts"> <script setup lang="ts">
import type { models } from '#imports'; import type { models } from '#imports';
import msg from '@veypi/msg'; import msg from '@veypi/msg';
import oaer from '@veypi/oaer';
let user = useUserStore()
let apps = ref<models.App[]>([]); let allApps = ref<models.App[]>([]);
let ofApps = ref<models.App[]>([]); let apps = computed(() => allApps.value.filter(e => e.user_status !== 'ok'))
let ofApps = computed(() => allApps.value.filter(e => e.user_status === 'ok'))
function getApps() { function getApps() {
api.app.List({}).then( api.app.List({}).then(
(e) => { (e) => {
apps.value = e; allApps.value = e;
api.app.AppUserList("*", { user_id: user.id }).then(aus => {
for (let i in aus) {
let ai = apps.value.findIndex(a => a.id === aus[i].app_id)
if (ai >= 0) {
if (aus[i].status === "ok") {
ofApps.value.push(apps.value[ai])
apps.value.splice(ai, 1)
}
}
}
})
} }
); );
} }
@ -108,7 +98,7 @@ let rules = {
function create_new() { function create_new() {
api.app.Post({ api.app.Post({
name: temp_app.value.name, icon: temp_app.value.icon, name: temp_app.value.name, icon: temp_app.value.icon,
des: "", participate: "auto" des: "", typ: "public", status: "ok"
}).then((e: models.App) => { }).then((e: models.App) => {
ofApps.value.push(e); ofApps.value.push(e);
msg.Info("创建成功"); msg.Info("创建成功");

@ -94,8 +94,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import msg from '@veypi/msg'; import msg from '@veypi/msg';
import * as crypto from 'crypto-js' import * as crypto from 'crypto-js'
import oaer from '../../oaer/lib/main' import oaer from '@veypi/oaer'
import { isValid } from 'js-base64';
definePageMeta({ definePageMeta({
@ -160,10 +159,10 @@ const login = () => {
redirect("") redirect("")
}).catch((e) => { }).catch((e) => {
console.warn(e) console.warn(e)
msg.Warn('登录失败:' + (e.err || e)) msg.Warn('登录失败:' + (e?.err || e))
}) })
}).catch(e => { }).catch(e => {
msg.Warn('登录失败:' + (e.err || e)) msg.Warn('登录失败:' + (e?.err || e))
}) })
}).catch(e => { }).catch(e => {
if (e.code === 40401) { if (e.code === 40401) {
@ -241,7 +240,7 @@ function redirect(url: string) {
if (uuid.value && uuid.value !== app.id) { if (uuid.value && uuid.value !== app.id) {
api.app.Get(uuid.value as string).then((app) => { api.app.Get(uuid.value as string).then((app) => {
api.token.Post({ api.token.Post({
token: localStorage.getItem('refresh') || '', refresh: '',
user_id: uuid.value as string, user_id: uuid.value as string,
app_id: uuid.value as string, app_id: uuid.value as string,
}).then(e => { }).then(e => {

@ -0,0 +1,15 @@
/*
* third.ts
* Copyright (C) 2024 veypi <i@veypi.com>
* 2024-10-29 19:52
* Distributed under terms of the GPL license.
*/
import oneicon from '@veypi/one-icon'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(oneicon)
})

File diff suppressed because it is too large Load Diff

@ -162,7 +162,7 @@ mxConstants.SHADOW_OPACITY = 0.25;
mxConstants.SHADOWCOLOR = '#000000'; mxConstants.SHADOWCOLOR = '#000000';
mxConstants.VML_SHADOWCOLOR = '#d0d0d0'; mxConstants.VML_SHADOWCOLOR = '#d0d0d0';
mxCodec.allowlist = ['mxStylesheet', 'Array', 'mxGraphModel', 'html', mxCodec.allowlist = ['mxStylesheet', 'Array', 'mxGraphModel',
'mxCell', 'mxGeometry', 'mxRectangle', 'mxPoint', 'mxCell', 'mxGeometry', 'mxRectangle', 'mxPoint',
'mxChildChange', 'mxRootChange', 'mxTerminalChange', 'mxChildChange', 'mxRootChange', 'mxTerminalChange',
'mxValueChange', 'mxStyleChange', 'mxGeometryChange', 'mxValueChange', 'mxStyleChange', 'mxGeometryChange',

@ -3,27 +3,25 @@ window.urlParams = window.urlParams || {};
// Public global variables // Public global variables
window.DOM_PURIFY_CONFIG = window.DOM_PURIFY_CONFIG || window.DOM_PURIFY_CONFIG = window.DOM_PURIFY_CONFIG ||
{ {ADD_TAGS: ['use'], FORBID_TAGS: ['form'],
ADD_TAGS: ['use'], FORBID_TAGS: ['form'], ALLOWED_URI_REGEXP: /^((?!javascript:).)*$/i,
ALLOWED_URI_REGEXP: /^((?!javascript:).)*$/i, ADD_ATTR: ['target', 'content']};
ADD_ATTR: ['target', 'content']
};
// Public global variables // Public global variables
window.MAX_REQUEST_SIZE = window.MAX_REQUEST_SIZE || 10485760; window.MAX_REQUEST_SIZE = window.MAX_REQUEST_SIZE || 10485760;
window.MAX_AREA = window.MAX_AREA || 15000 * 15000; window.MAX_AREA = window.MAX_AREA || 15000 * 15000;
// URLs for save and export // URLs for save and export
window.EXPORT_URL = window.EXPORT_URL || './drawio'; window.EXPORT_URL = window.EXPORT_URL || './drawio_demo';
window.SAVE_URL = window.SAVE_URL || './drawio'; window.SAVE_URL = window.SAVE_URL || './drawio_demo';
window.PROXY_URL = window.PROXY_URL || null; window.PROXY_URL = window.PROXY_URL || null;
window.OPEN_URL = window.OPEN_URL || './drawio'; window.OPEN_URL = window.OPEN_URL || './drawio_demo';
window.RESOURCES_PATH = window.RESOURCES_PATH || './drawio/resources'; window.RESOURCES_PATH = window.RESOURCES_PATH || './drawio_demo/resources';
window.STENCIL_PATH = window.STENCIL_PATH || './drawio/image/stencils'; window.STENCIL_PATH = window.STENCIL_PATH || './drawio_demo/image/stencils';
window.IMAGE_PATH = window.IMAGE_PATH || './drawio/image'; window.IMAGE_PATH = window.IMAGE_PATH || './drawio_demo/image';
window.STYLE_PATH = window.STYLE_PATH || './drawio/src/css'; window.STYLE_PATH = window.STYLE_PATH || './drawio_demo/src/css';
window.CSS_PATH = window.CSS_PATH || './drawio/src/css'; window.CSS_PATH = window.CSS_PATH || './drawio_demo/src/css';
window.OPEN_FORM = window.OPEN_FORM || './drawio'; window.OPEN_FORM = window.OPEN_FORM || './drawio_demo';
window.mxBasePath = window.mxBasePath || './drawio/src'; window.mxBasePath = window.mxBasePath || './drawio_demo/src';
window.mxLanguage = window.mxLanguage || window.RESOURCES_PATH + '/zh'; window.mxLanguage = window.mxLanguage || window.RESOURCES_PATH + '/zh';
window.mxLanguages = window.mxLanguages || ['zh']; window.mxLanguages = window.mxLanguages || ['zh'];

@ -753,4 +753,4 @@ softwaredesign=软件设计图
venndiagrams=文氏图 venndiagrams=文氏图
webEmailOrOther=网站、电邮或其他网络地址 webEmailOrOther=网站、电邮或其他网络地址
webLink=Web链接 webLink=Web链接
wireframes=线框图 wireframes=线框图

@ -6,7 +6,7 @@
*/ */
import { Base64 } from 'js-base64' import { Base64 } from 'js-base64'
import type { auth, models } from '~/composables'; import { models, type auth } from '~/composables';
export const useUserStore = defineStore('user', { export const useUserStore = defineStore('user', {
state: () => ({ state: () => ({

Loading…
Cancel
Save