初始化magic-boot项目

This commit is contained in:
李龙龙 2024-06-14 14:57:55 +08:00
commit b6b426097b
177 changed files with 15420 additions and 0 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

18
.idea/compiler.xml Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="magic-boot" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="magic-boot" options="-parameters" />
</option>
</component>
</project>

19
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="magic-boot@manage.zh-cc.top" uuid="f6959f5a-43fe-4eb9-aae5-284f2c2dfea4">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/magic-boot/src/main/resources/application.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://manage.zh-cc.top:14725/magic-boot?useSSL=false&amp;zeroDateTimeBehavior=convertToNull&amp;useUnicode=true&amp;characterEncoding=UTF8&amp;autoReconnect=true&amp;allowPublicKeyRetrieval=true&amp;serverTimezone=Asia/Shanghai</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

6
.idea/encodings.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/magic-boot/src/main/java" charset="UTF-8" />
</component>
</project>

20
.idea/jarRepositories.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

9
.idea/magic.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

14
.idea/misc.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/magic-boot/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_22" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/magic.iml" filepath="$PROJECT_DIR$/.idea/magic.iml" />
</modules>
</component>
</project>

6
.idea/sqldialects.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="MySQL" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1 @@
VITE_APP_BASE_API = 'http://127.0.0.1:8089/'

View File

@ -0,0 +1,2 @@
#VITE_APP_BASE_API = 'http://localhost:8089/'
VITE_APP_BASE_API = 'http://192.168.1.9:8089/'

53
magic-boot-naive/.gitignore vendored Normal file
View File

@ -0,0 +1,53 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
tests/**/coverage/
# Editor directories and files
*.suo
*.ntvs*
*.njsproj
*.sln
*.log
logs/

21
magic-boot-naive/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 吕金泽
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,52 @@
##
后端:
<span style="margin-right: 5px">
<a href="https://gitee.com/ssssssss-team/magic-boot" target="_blank"><img src="https://gitee.com/ssssssss-team/magic-boot/badge/star.svg?theme=white"></a>
</span>
<a href="https://github.com/zegezy/magic-boot" target="_blank"><img src="https://img.shields.io/github/stars/zegezy/magic-boot.svg?style=social"></a>
前端:
<span style="margin-right: 5px">
<a href="https://gitee.com/ssssssss-team/magic-boot-naive" target="_blank"><img src="https://gitee.com/ssssssss-team/magic-boot-naive/badge/star.svg?theme=white"></a>
</span>
<a href="https://github.com/zegezy/magic-boot-naive" target="_blank"><img src="https://img.shields.io/github/stars/zegezy/magic-boot-naive.svg?style=social"></a>
## 简介
基于[ **magic-api** ](https://gitee.com/ssssssss-team/magic-api)搭建的快速开发平台前端采用Vue3 + naive-ui最新版本搭建依赖较少运行速度快。对常用组件进行封装。利用Vue3的`@vue/compiler-sfc`单文件编译动态编译组件可以实现在浏览器编写Vue代码既改即生效快速开发。
QQ群[ **576433387** ](https://jq.qq.com/?_wv=1027&k=KD6DPvB0)
| 代码 | 效果 |
|----|----|
| ![](https://magicboot.oss-cn-beijing.aliyuncs.com/code.png) | ![](https://magicboot.oss-cn-beijing.aliyuncs.com/system/1.png) |
## 功能
- 菜单管理:树结构,配置菜单、权限按钮、选择关联组件等,支持全局模糊搜索
- 组织机构:树结构,配置组织机构,类型:部门、公司(选择项存在数据字典),支持全局模糊搜索
- 角色管理:角色菜单权限分配、设置数据范围等。
- 用户管理:用户添加、导入用户(支持导入前预览数据)、登录状态更改等
- 数据字典:分为`系统类`和`业务类`两类数据维护
- 动态组件在线编写Vue3业务页面代码保存编译生效
- 操作日志:接口调用操作日志查询
- 登录日志:系统登录日志查询,包含登录失败日志
- 数据库监控Druid Monitor SQL监控、数据源信息查看等
- 在线用户:当前活跃在系统内的用户,可以选择踢人下线
## 在线体验
- 演示地址:
前台:[ **https://preview.magicboot.net/** ](https://preview.magicboot.net/)
后台:[ **https://api.magicboot.net:8443/magic/web/index.html** ](https://api.magicboot.net:8443/magic/web/index.html)
- 文档地址:[ **https://magicboot.net/** ](https://magicboot.net/)
- 账号system/123456
## 系统截图
| ![](https://magicboot.oss-cn-beijing.aliyuncs.com/system/1.png) | ![](https://magicboot.oss-cn-beijing.aliyuncs.com/system/2.png) |
|---|---|
| ![](https://magicboot.oss-cn-beijing.aliyuncs.com/system/3.png) | ![](https://magicboot.oss-cn-beijing.aliyuncs.com/system/4.png) |
| ![](https://magicboot.oss-cn-beijing.aliyuncs.com/system/5.png) | ![](https://magicboot.oss-cn-beijing.aliyuncs.com/system/6.png) |
| ![](https://magicboot.oss-cn-beijing.aliyuncs.com/system/7.png) | ![](https://magicboot.oss-cn-beijing.aliyuncs.com/system/8.png) |
| ![](https://magicboot.oss-cn-beijing.aliyuncs.com/system/9.png) | ![](https://magicboot.oss-cn-beijing.aliyuncs.com/system/10.png) |

View File

@ -0,0 +1,2 @@
del /f/s/q node_modules > nul;
rmdir /s/q node_modules;

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" href="/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title></title>
<style>
html, body, #app {
width: 100%;
height: 100%;
margin: 0px;
padding: 0px;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,47 @@
{
"name": "magic-boot-vite",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"demo": "vite build --mode demo"
},
"dependencies": {
"@layui/layer-vue": "^2.3.2",
"@volar/monaco": "1.7.9",
"@vueuse/core": "^10.1.0",
"ali-oss": "^6.17.1",
"axios": "^0.24.0",
"js-sha256": "^0.9.0",
"less": "^4.1.3",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"monaco-editor-core": "^0.46.0",
"monaco-editor-textmate": "^4.0.0",
"monaco-textmate": "^3.0.1",
"monaco-volar": "^0.4.0",
"onigasm": "^2.2.5",
"path-browserify": "^1.0.1",
"pinia": "^2.0.33",
"vue": "^3.3.11",
"vue-cropper": "^1.0.2",
"vue-router": "^4.1.6",
"vuedraggable": "^4.1.0",
"xlsx": "^0.18.3"
},
"devDependencies": {
"@vicons/antd": "^0.12.0",
"@vicons/fluent": "^0.12.0",
"@vicons/ionicons5": "^0.12.0",
"@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13",
"naive-ui": "^2.38.1",
"postcss": "^8.4.21",
"sortablejs": "^1.15.0",
"tailwindcss": "^3.2.7",
"vite": "^4.1.0",
"vite-plugin-svg-icons": "^1.1.0"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,29 @@
<template>
<n-config-provider
:locale="zhCN"
:date-locale="dateZhCN"
v-bind="configProvider"
>
<n-message-provider>
<n-dialog-provider>
<router-view/>
</n-dialog-provider>
</n-message-provider>
</n-config-provider>
</template>
<script setup>
import { reactive } from 'vue'
import {NConfigProvider, zhCN, dateZhCN} from 'naive-ui'
import global from '@/scripts/global'
const configProvider = reactive({})
configProvider.themeOverrides = global.selectTheme.themeOverrides
</script>
<style lang="less">
@import 'styles/index.less';
.n-config-provider{
height: 100%;
}
</style>

View File

@ -0,0 +1,23 @@
import {useDictStore} from "@/store/modules/dictStore";
const dictStore = useDictStore()
export function getCheckboxData(props){
let handlerData = (dictData) => {
if(props.optionsFilter){
dictData = dictData.filter(props.optionsFilter)
}
return dictData
}
return new Promise((resolve, reject) => {
if (props.type) {
resolve(handlerData(dictStore.getDictType(props.type)))
} else if (props.url) {
$common.get(props.url, props.params).then(res => {
resolve(handlerData($common.mapLabelValue((res.data.list || res.data), props.labelField, props.valueField)))
})
} else if (props.options && props.options.length > 0) {
resolve(handlerData($common.mapLabelValue(props.options, props.labelField, props.valueField)))
} else{
reject()
}
})
}

View File

@ -0,0 +1,43 @@
import {useDictStore} from "@/store/modules/dictStore";
const dictStore = useDictStore()
export function getSelectData(props){
let listConcat = (dictData) => {
let selectList = []
if(props.optionsFilter){
dictData = dictData.filter(props.optionsFilter)
}
if (props.allOption) {
selectList = [{
value: '',
label: '全部'
}]
selectList = selectList.concat(dictData)
} else {
selectList = dictData
}
return selectList
}
let handlerData = (data) => {
let newData = []
data.forEach(it => {
newData.push({
label: it[props.labelField || 'label'],
value: it[props.valueField || 'value'].toString()
})
})
return newData
}
return new Promise((resolve, reject) => {
if (props.type) {
resolve(listConcat(dictStore.getDictType(props.type)))
} else if (props.url) {
$common.get(props.url, props.params).then(res => {
resolve(listConcat(handlerData(res.data.list || res.data)))
})
} else if (props.options && props.options.length > 0) {
resolve(listConcat(handlerData(props.options)))
} else{
reject()
}
})
}

View File

@ -0,0 +1,11 @@
import treeTable from "@/scripts/treeTable";
export function getTreeSelectData(props){
return new Promise(resolve => {
$common.get(props.url).then(res => {
let options = res.data.list
treeTable.deleteEmptyChildren(options)
resolve(options)
})
})
}

View File

@ -0,0 +1,27 @@
::-webkit-scrollbar {
width: 6px;
height: 6px;
overflow: auto
}
::-webkit-scrollbar-thumb {
background-color: #e6e6e6;
min-height: 25px;
min-width: 25px;
border: 1px solid #e0e0e0;
border-radius: 99px
}
::-webkit-scrollbar-track {
background-color: #f7f7f7;
border: 1px solid #efefef
}
body{
--mb-main-icon-color: #909399;
}
.clear{
clear: both;
}
a{
text-decoration: none;
}

View File

@ -0,0 +1,10 @@
import {defineAsyncComponent} from 'vue'
const components = import.meta.glob('./**/*.vue')
export function setupComponents(app) {
for (const [key, value] of Object.entries(components)) {
const name = key.substring(key.lastIndexOf('/') + 1, key.lastIndexOf('.'))
app.component(name, defineAsyncComponent(value))
}
}

View File

@ -0,0 +1,39 @@
export default {
table: {
// 选中行颜色
selectedRowColor: '#D9DDE2',
// 单元格内容不换行显示
nowrap: true,
// todo 拖拽列之后回调此方法,用保存列
// saveCols(tableId, columns) {
//
// },
// todo 远程加载列
// remoteLoadColumn() {
//
// },
// todo 保存页码数
// savePage(pageSize,tableId){
//
// },
// todo 获取远程页码数
// async getPage(tableId, callback){
//
// },
// 下拉表格选项
dropMenus: [],
// 表头提示
titleTooltip:{
iconProps: {
icon: 'QuestionCircleFilled',
color: '#4b6fa7'
}
},
// 单元格显示的图片
image:{
width: 30,
height: 30
}
}
}

View File

@ -0,0 +1,56 @@
<template>
<svg
v-if="dynamic"
aria-hidden="true"
class="mb-icon"
:fill="color"
:style="{ width: size, height: size }"
>
<use :xlink:href="symbolId" :class="className"/>
<title>{{ title }}</title>
</svg>
<n-icon
v-else
:color="color"
:size="size"
:title="title"
>
<component :is="xicons[icon]" />
</n-icon>
</template>
<script setup>
import { computed, ref } from 'vue'
import xicons from '@/scripts/xicons'
import svgIcons from '@/scripts/svg-icons'
const props = defineProps({
prefix: {
type: String,
default: 'mb-icon'
},
icon: String,
size: {
type: String,
default: '1em'
},
color: {
type: String,
default: 'currentColor'
},
title: {
type: String,
default: ''
}
});
const symbolId = computed(() => props.icon&&props.icon.startsWith('#') ? props.icon : `#${props.prefix}-${props.icon}`)
const className = computed(() => props.icon&&props.icon.startsWith('#') ? props.icon.substring(1) : `${props.prefix}-${props.icon}`)
const dynamic = ref(false)
if(svgIcons.indexOf(props.icon) != -1){
dynamic.value = true
}
</script>
<style scoped>
.mb-icon {
vertical-align: -0.25em;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,640 @@
<style scoped>
.edit-text{
flex: 1;
}
:deep(.n-data-table-tr:hover .edit-text){
border: var(--mb-editor-table-tr-hover-border)
}
.edit-text:hover{
border: 1px dashed #ccc !important;
}
.edit-text-not-allowed{
cursor: not-allowed;
}
:deep(.n-data-table-td){
height: 60px;
}
:deep(.n-data-table .n-data-table-expand-trigger),
:deep(.n-data-table .n-data-table-indent),
:deep(.n-data-table .n-data-table-expand-placeholder){
float: left;
}
.copy-text{
margin: 0px 5px;
cursor: pointer;
}
</style>
<template>
<mb-table
ref="magicTable"
v-bind="tableOptions"
@scroll="onScroll"
@dynamicSettingContextmenu="dynamicSettingContextmenu"
@contextmenuSelect="contextmenuSelect"
>
<template v-for="(col, colIndex) in cols" #[col.field]="{ row }">
<template v-if="row">
<template v-if="col.component">
<!-- 设置了组件并且是非编辑模式下 -->
<template v-if="col.alwaysEdit !== true && !edits[row._index_ + '' + colIndex]">
<div v-if="getIsEdit(col.edit, row)" class="flex items-center h-4/5">
<!-- 如果是可以编辑则鼠标悬浮显示边框 -->
<div
@click="editMode(row._index_, colIndex, col, row)"
class="edit-text h-full"
:style="col.labelStyle"
>
<slot
:name="col.field + '-view'"
:row="row"
:col="col"
:row-index="row._index_"
:col-index="colIndex"
>
<span v-if="col.show == undefined || (col.show && col.show(row))">
{{ getLabel(row[col.field], col) }}
</span>
</slot>
</div>
<mb-icon v-if="col.copyText" class="copy-text" icon="CopyOutline" @click="common.copyText(getLabel(row[col.field], col))" />
</div>
<div v-else class="flex items-center">
<!-- 如果不可以编辑则鼠标悬浮显示禁止标志 -->
<div class="edit-text-not-allowed flex-1" :style="col.labelStyle">
<slot
:name="col.field + '-view'"
:row="row"
:col="col"
:row-index="row._index_"
:col-index="colIndex"
>
<span v-if="col.show == undefined || (col.show && col.show(row))">
{{ common.getValidValue(getLabel(row[col.field], col), '-') }}
</span>
</slot>
</div>
<mb-icon v-if="col.copyText" class="copy-text" icon="CopyOutline" @click="common.copyText(common.getValidValue(getLabel(row[col.field], col), '-'))" />
</div>
</template>
<div class="flex items-center" v-if="(col.edit != undefined && col.edit(row) && col.alwaysEdit) || (col.edit == undefined && col.alwaysEdit) || (edits[row._index_ + '' + colIndex] && currentRowIndex == row._index_ && currentColIndex == colIndex)">
<div class="flex-1">
<!-- edit alwaysEdit 可配合使用 比如符合条件的 可以一直保持编辑模式 -->
<slot
:name="col.field + '-edit'"
:row="row"
:col="col"
:row-index="row._index_"
:col-index="colIndex"
>
<!-- edit = true始终编辑模式 或者激活编辑模式 显示组件 -->
<component
v-if="!col.component.startsWith('n-')"
:ref="(el) => setComponentRef(row._index_, colIndex, el, col)"
:is="col.component.startsWith('#') ? col.component.substring(1) : 'mb-' + col.component"
v-model="row[col.field]"
v-bind="componentDynamicBind(row, col)"
:style="col.componentStyle"
@blur="componentBlur(row._index_, colIndex, col, row)"
:close-current-col-edit-mode="closeCurrentColEditMode"
/>
<!-- naive组件 大多使用 v-model:value 绑定主要是兼容这个 -->
<component
v-else
:ref="(el) => setComponentRef(row._index_, colIndex, el, col)"
:is="col.component"
v-model:value="row[col.field]"
v-bind="componentDynamicBind(row, col)"
:style="col.componentStyle"
@blur="componentBlur(row._index_, colIndex, col, row)"
/>
</slot>
</div>
<mb-icon v-if="col.copyText" class="copy-text" icon="CopyOutline" @click="common.copyText(row[col.field])" />
</div>
</template>
<div v-else class="flex items-center">
<!-- 如果没有设置组件 直接显示数据 -->
<div :style="col.labelStyle" class="flex-1 w-full">
<slot
:name="col.field + '-view'"
:row="row"
:col="col"
:row-index="row._index_"
:col-index="colIndex"
>
<mb-table-tooltip v-if="col.show == undefined || (col.show && col.show(row))">
{{ row[col.field] }}
</mb-table-tooltip>
</slot>
</div>
<mb-icon v-if="col.copyText" class="copy-text" icon="CopyOutline" @click="common.copyText(row[col.field])" />
</div>
</template>
</template>
</mb-table>
</template>
<script setup>
import { reactive, ref, nextTick, toRaw, watch } from 'vue'
import { getSelectData } from "@/api/components/mb-select.js";
import { getTreeSelectData } from "@/api/components/mb-tree-select";
import { omit, cloneDeep } from 'lodash-es'
import treeTable from "@/scripts/treeTable";
import common from '@/scripts/common'
const magicTable = ref()
const props = defineProps({
id: {
type: String,
default: ''
},
props: {
type: Object,
default: () => {}
},
cols: {
type: Array,
default: () => []
},
showNo: {
type: Boolean,
default: true
},
operation: {
type: Object,
default: () => {}
},
operationWidth: {
type: Number,
default: 85
},
page: {
type: Boolean,
default: false
},
rowKey: {
type: String,
default: 'id'
},
preview: {
type: Boolean,
default: false
},
rowHoverEdit: {
type: Boolean,
default: true
},
keepCurrentPage: {
type: Boolean,
default: true
}
})
//
const tableOptions = reactive({
id: props.id,
page: props.page,
showNo: props.showNo,
selectedRowEnable: false,
contextmenuEnable: true,
rowKey: props.rowKey,
keepCurrentPage: props.keepCurrentPage,
data: [],
cols: [],
props: props.props
})
const currentColIndex = ref(0)
const currentRowIndex = ref(0)
const currentCol = ref()
const currentRow = ref()
//
const edits = ref({})
const showLabelData = reactive({})
const disableComponentCallbackFields = ref([])
function setData(data){
let newData = cloneDeep(data)
tableOptions.data = dataAddIndex(newData)
}
for (let i in props.cols) {
let col = props.cols[i]
getShowLabelData(col)
if(!col.type){
col.type = 'dynamic'
}
if(!col.editIcon){
col.editIcon = col.component ? true : false
}
if(col.copyAll){
col.copyAllCallback = (col) => {
let labels = []
treeTable.recursionRearrange(getData()).forEach(it => {
labels.push(common.getValidValue(getLabel(it[col.field], col), '-'))
})
$common.copyText(labels.join('\n'))
}
}
tableOptions.cols.push(col)
}
function getShowLabelData(col){
switch (col.component) {
case 'select':
getSelectData(col.componentProps).then(data => {
showLabelData[col.field] = data
})
break;
case 'tree-select':
getTreeSelectData(col.componentProps).then(data => {
showLabelData[col.field] = treeTable.recursionRearrange(data)
})
break;
default:
break;
}
}
let buttons = []
if(props.operation && !props.preview){
let deleteClick = (row, confirm) => {
if(confirm){
$common.warning('此操作将永久删除该数据, 是否继续?', () => {
deleteRow(row)
})
}else{
deleteRow(row)
}
}
let deleteType = props.operation.delete instanceof Object
let subType = props.operation.sub instanceof Object
let sameType = props.operation.same instanceof Object
buttons.push(...[{
label: '删除',
link: true,
icon: 'Delete24Regular',
if: (row) => {
return deleteType ? props.operation.delete.if === undefined || (props.operation.delete.if && props.operation.delete.if(row)) : props.operation.delete
},
click: (row) => {
deleteType ? deleteClick(row, props.operation.delete.confirm) : deleteClick(row)
}
}, {
label: '添加下级',
link: true,
icon: 'sub-level',
if: (row) => {
return subType ? props.operation.sub.if === undefined || (props.operation.sub.if && props.operation.sub.if(row)) : props.operation.sub
},
click: (row) => {
addChildrenRow(row)
}
}, {
label: '添加同级',
link: true,
icon: 'same-level',
if: (row) => {
return sameType ? props.operation.same.if === undefined || (props.operation.same.if && props.operation.same.if(row)) : props.operation.same
},
click: (row) => {
addRow(row)
}
}])
if(props.operation.buttons){
buttons.push(...props.operation.buttons)
}
tableOptions.cols.push({
label: '操作',
type: 'buttons',
width: props.operationWidth,
fixed: 'right',
buttons: buttons
})
}
function componentDynamicBind(row, col){
let bind = {...col.componentProps}
if(col.componentProps){
for(let key in col.componentProps){
if(key.startsWith('on') && typeof(col.componentProps[key]) == 'function'){
bind[key] = (...data) => {
if(disableComponentCallbackFields.value.indexOf(col.field) == -1){
let _data = {
editorCurrentRow: row
}
for(let key in data){
// key_data
if(data[key] instanceof Object && data[key]['_deconstruction_']){
for(let key2 in data[key]){
_data[key2] = data[key][key2]
}
}else{
_data[key] = data[key]
}
}
col.componentProps[key](_data)
}
}
}
}
}
if(col.handlerComponentProps){
col.handlerComponentProps(bind, row)
}
return bind
}
//
function addRow(row){
// push
recursionAddRow(tableOptions.data, row._index_)
tableDataAddIndex()
}
function recursionAddRow(children, index){
children.forEach(it => {
if(it._index_ == index){
children.push({[props.rowKey]: $common.uuid()})
}
if(it.children && it.children.length > 0){
recursionAddRow(it.children, index)
}
})
}
//
function addChildrenRow(row){
if(row.children && row.children.length > 0){
row.children.push({[props.rowKey]: $common.uuid()})
}else{
row.children = [{[props.rowKey]: $common.uuid()}]
}
tableDataAddIndex()
}
/**
* 递归增加index主要是为了解决树形结构数据naive-ui的index不包含子级的问题
* https://www.naiveui.com/zh-CN/os-theme/components/data-table#expand.vue
*/
function dataAddIndex(children){
let index = { index: 0 }
//
let data = cloneDeep(children)
recursionAddIndex(data, index)
return data
}
function tableDataAddIndex(){
tableOptions.data = dataAddIndex(tableOptions.data)
}
function recursionAddIndex(children, index){
children.forEach(it => {
it._index_ = index.index
index.index++
if(it.children && it.children.length > 0){
recursionAddIndex(it.children, index)
}
})
}
//
function deleteRow(row){
recursionDelete(tableOptions.data, row._index_)
tableDataAddIndex()
let deleteAfter = props.operation?.delete?.deleteAfter;
deleteAfter && deleteAfter()
}
function recursionDelete(children, index){
children.forEach((it, i) => {
if(it._index_ == index){
children.splice(i, 1)
}
if(it.children && it.children.length > 0){
recursionDelete(it.children, index)
}
})
}
// editboolean editfunction
function getIsEdit(edit, row){
if(props.preview){
return false
}
if(typeof(edit) == 'function'){
return edit(row)
}
return true
}
function getLabelByData({col, value, data, valueField, labelField}){
let dataList = data || showLabelData[col.field]
if(dataList && dataList.length > 0){
let labels = []
let values = value.toString().split(',')
for(let value of values){
let data = dataList.filter(it => it[col.showLabel?.valueField || valueField] == value)[0];
labels.push(data && data[col.showLabel?.labelField || labelField])
}
return labels.join(',')
}
}
//
function getLabel(value, col){
if($common.notEmptyNot01(value)){
let valueField = col.componentProps?.valueField || 'value'
let labelField = col.componentProps?.labelField || 'label'
if(['select', 'tree-select'].indexOf(col.component) != -1){
if(col.component == 'tree-select'){
valueField = col.componentProps?.valueField || 'key'
labelField = col.componentProps?.labelField || 'label'
}else if(col.component == 'select'){
valueField = 'value'
labelField = 'label'
}
return getLabelByData({col, value, valueField, labelField})
}else if(col.showLabel){
return getLabelByData({col, value, data: col.showLabel.data, valueField, labelField})
}else{
return value
}
}
}
let currentEditRef = null
function componentBlur(rowIndex, colIndex, col, row){
edits.value[rowIndex + '' + colIndex] = false
if(disableComponentCallbackFields.value.indexOf(col.field) == -1){
col.blur && col.blur(row)
}
}
//
function closeCurrentColEditMode(){
componentBlur(currentRowIndex.value, currentColIndex.value, currentCol.value, currentRow.value)
}
// ref
function setComponentRef(rowIndex, colIndex, el, col){
if(el && edits.value[rowIndex + '' + colIndex]){
currentEditRef = el
nextTick(() => {
if(el.focus){
el.focus()
}else{
let key = Object.keys(toRaw(el.$refs))[0]
// focustemplatefocus refmb-input
if(key){
if(el.$refs[key].focus){
el.$refs[key].focus()
}else{
el.$refs[key].$el.focus()
}
}
}
componentInit(el, col)
})
}
}
//
function componentInit(el, col){
switch (col.component){
case 'textarea':
//
textareaInit(el, col)
break;
case 'tree-select':
//
el.expand()
break;
default:
break;
}
}
// textarea
function textareaInit(el, col){
col.componentStyle = col.componentStyle || {}
let parentNodeRect = el.$el.parentNode.getBoundingClientRect()
let tableRect = magicTable.value.$el.getBoundingClientRect()
let tableWidth = magicTable.value.$el.clientWidth
let left = parentNodeRect.left - tableRect.left
let top = parentNodeRect.top - tableRect.top - magicTable.value.$el.querySelector('.n-data-table-base-table-header').offsetHeight
col.componentStyle.position = 'absolute'
col.componentStyle['z-index'] = 999999
col.componentStyle.width = col.componentStyle.width || parentNodeRect.width + 'px'
if(tableWidth - left - col.componentStyle.width.match(/\d+/)[0] < 1){
col.componentStyle.right = `1px`
col.componentStyle.left = 'unset'
}else{
col.componentStyle.left = `${left}px`
col.componentStyle.right = 'unset'
}
col.componentStyle.top = `${top}px`
}
//
function previewMode(rowIndex, colIndex){
edits.value[rowIndex + '' + colIndex] = false
}
//
function editMode(rowIndex, colIndex, col, row){
currentRowIndex.value = rowIndex
currentColIndex.value = colIndex
edits.value[rowIndex + '' + colIndex] = true
if(col){
currentCol.value = col
}
if(row){
currentRow.value = row
}
}
function onScroll(){
if(currentCol.value && currentCol.value.component == 'textarea' && edits.value[currentRowIndex.value + '' + currentColIndex.value]){
edits.value[currentRowIndex.value + '' + currentColIndex.value] = false
currentCol.value = null
}
}
let contextmenus = []
function dynamicSettingContextmenu(row){
contextmenus = []
for(let button of buttons){
if((button.if && button.if(row)) || button.if === undefined){
contextmenus.push({
key: button.label,
title: button.label,
row: row,
click: button.click,
icon: $common.renderIcon(button.icon)
})
}
}
return contextmenus
}
function contextmenuSelect(key){
let menu = contextmenus.filter(it => it.key == key)[0]
if(menu.click){
menu.click(menu.row)
}
}
function getData(){
return tableOptions.data.map(it => omit({...it}, '_index_'))
}
function push(data){
tableOptions.data.push(data)
tableDataAddIndex()
}
function unshift(data){
tableOptions.data.unshift(data)
tableDataAddIndex()
}
function getTableRef(){
return magicTable.value
}
function disableComponentCallback(fields){
disableComponentCallbackFields.value.push(...fields)
}
function enableComponentCallback(fields){
for(let field of fields){
let index = disableComponentCallbackFields.value.indexOf(field)
if(index != -1){
disableComponentCallbackFields.value.splice(index, 1)
}
}
}
watch(magicTable, () => {
if(props.rowHoverEdit){
magicTable.value.$el.style.setProperty('--mb-editor-table-tr-hover-border', '1px dashed #ccc')
}else{
magicTable.value.$el.style.setProperty('--mb-editor-table-tr-hover-border', 'unset')
}
})
defineExpose({
getTableRef,
previewMode,
editMode,
getData,
setData,
push,
unshift,
disableComponentCallback,
enableComponentCallback,
closeCurrentColEditMode
})
</script>

View File

@ -0,0 +1,40 @@
<template>
<div class="pagination-container" style="height: 50px">
<n-pagination
:page="page"
:page-size="pageSize"
:item-count="itemCount"
:page-sizes="[10, 20, 50, 100, 200]"
@updatePage="emit('update-page', $event)"
@updatePageSize="emit('update-page-size', $event)"
show-quick-jumper
show-size-picker
style="margin: 0 auto;"
/>
</div>
</template>
<script setup>
const props = defineProps({
page: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
},
itemCount: {
type: Number,
default: undefined
}
})
const emit = defineEmits(['update-page', 'update-page-size'])
</script>
<style scoped>
.pagination-container{
display: flex;
align-items: center;
}
</style>

View File

@ -0,0 +1,69 @@
<!--
Magic-Boot 单选字典组件
@author Yean
-->
<template>
<n-radio-group :size="$global.uiSize.value" :value="modelValue" @change="handleChange">
<n-radio-button v-for="item in dictList" :key="item.value" :value="item.value" v-if="isButton">
{{ item.label }}
</n-radio-button>
<n-radio v-for="item in dictList" :key="item.value" :value="item.value" v-else>{{ item.label }}</n-radio>
</n-radio-group>
</template>
<script setup>
import {ref, watch} from "vue";
import {useDictStore} from "@/store/modules/dictStore";
const emits = defineEmits(['update:modelValue'])
const dictStore = useDictStore();
const modelValue = ref();
const dictList = ref([]);
const props = defineProps({
//
modelValue: {
type: [String, Number],
default: ''
},
// Key
dictKey: {
type: String,
default: ''
},
//
isButton: {
type: Boolean,
default: false
}
})
if (props.dictKey) {
dictList.value = dictStore.getDictType(props.dictKey);
}
if (props.modelValue) {
modelValue.value = props.modelValue;
}
watch(() => props.modelValue, value => {
modelValue.value = value;
})
watch(() => props.dictKey, value => {
if (value) {
dictList.value = dictStore.getDictType(value);
}
})
const handleChange = (e) => {
emits("update:modelValue", e.target.value)
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,136 @@
<template>
<n-form :size="$global.uiSize.value" inline label-placement="left" @keyup.enter="search" style="flex-wrap:wrap">
<template v-for="(it, i) in where">
<n-form-item v-if="formItemIf(it)" :label="it.label" :key="i" :show-feedback="false">
<component
:is="!it.component ? 'mb-input' : it.component.startsWith('n-') || 'mb-' + it.component"
v-model="it.value"
:item-label="it.label"
v-bind="it.componentProps || it.props"
/>
</n-form-item>
</template>
<n-form-item :show-feedback="false">
<n-space>
<n-button :size="$global.uiSize.value" type="primary" @click="search">
<mb-icon icon="Search" />
搜索
</n-button>
<n-button :size="$global.uiSize.value" @click="reset">
<mb-icon icon="TrashOutline" />
清空
</n-button>
<slot name="buttons"/>
</n-space>
</n-form-item>
</n-form>
</template>
<script setup>
import {nextTick, watch} from 'vue'
const props = defineProps({
where: {
type: Object,
default: () => {
}
},
notReset: {
type: String,
default: ''
}
})
function formItemIf(it){
if(it && it instanceof Object){
it.show = it.show === undefined ? !!it.label : it.show
return it.show
}
return false
}
function reRenderComponent(key){
props.where[key].show = false
nextTick(() => {
props.where[key].show = true
})
}
for (let key in props.where) {
if (props.where[key] instanceof Object) {
if (props.where[key].value === undefined) {
props.where[key].value = null
}
watch(() => props.where[key].value, (value) => {
props.where[key].valueChange && props.where[key].valueChange({value, reRenderComponent, where: props.where})
})
if (props.where[key].component === 'date') {
let isResetValue = false
for (let k in props.where[key]) {
if (k === 'resetValue') {
isResetValue = true
}
}
if (!isResetValue) {
// date reset null
props.where[key].resetValue = null
}
}
}
}
const emit = defineEmits(['search'])
function input(input) {
if (input) {
emit('search')
}
}
function search() {
for (let key in props.where) {
if (props.where[key] instanceof Object) {
if (props.where[key].component && props.where[key].component.startsWith('date') && props.where[key].value instanceof Array && props.where[key].value.join(',')) {
props.where[key].value = props.where[key].value.join(',')
}
}
}
nextTick(() => {
emit('search')
})
}
function reset() {
for (let key in props.where) {
if (props.notReset.indexOf(key) === -1) {
if (props.where[key] instanceof Object) {
let isResetValue = false
for (let k in props.where[key]) {
if (k === 'resetValue') {
isResetValue = true
}
}
if (isResetValue) {
props.where[key].value = props.where[key].resetValue
} else {
if (props.where[key].value instanceof Array) {
props.where[key].value = []
} else {
props.where[key].value = null
}
}
} else {
if (props.where[key] instanceof Array) {
props.where[key] = []
} else {
props.where[key] = null
}
}
}
}
nextTick(() => emit('search'))
}
defineExpose({ reRenderComponent })
</script>

View File

@ -0,0 +1,112 @@
<template>
<span>
<template v-if="type === 'switch'">
<mb-switch
v-model="row[col.field]"
@change="col.change(row)"
v-if="col.if != undefined ? col.if(row) : true"
:checked-value="col.checkedValue"
:unchecked-value="col.uncheckedValue"
v-bind="col.props"
/>
<span v-else>-</span>
</template>
<template v-else-if="type === 'html'">
<mb-table-tooltip :nowrap="nowrap">
<span v-html="getLabel(row, col)"></span>
</mb-table-tooltip>
<mb-icon v-if="col.copyText" class="copy-text" icon="CopyOutline" @click="copyText(getValueByPath(row, col.field))" />
</template>
<template v-else-if="type === 'templet'">
<mb-table-tooltip :nowrap="nowrap">
<span v-html="col.templet(row, col, index)"></span>
</mb-table-tooltip>
<mb-icon v-if="col.copyText" class="copy-text" icon="CopyOutline" @click="copyText(col.templet(row, col, index))" />
</template>
<template v-else-if="type === 'buttons'">
<n-space>
<template v-for="it in col.buttons">
<slot v-if="it.type == 'dynamic'" :name="it.field" :row="row" :index="index" />
<n-button
v-else-if="it.if != undefined ? it.if(row) : true"
v-permission="it.permission"
:type="it.type"
:text="it.link"
:dashed="it.dashed"
:href="it.href"
:color="it.color"
:target="it.target"
:tag="it.tag || (it.link ? 'a' : 'button')"
:text-color="it.textColor || '#2D8CF0'"
@click="it.click(row)"
>
<template #icon v-if="it.icon">
<mb-icon :icon="it.icon" />
</template>
{{ it.label }}
</n-button>
</template>
</n-space>
</template>
<template v-else-if="type === 'dictType'">
<mb-table-tooltip :nowrap="nowrap">
<span>{{ dictStore.getDictLabel(col.dictType, getValueByPath(row, col.field) + '') }}</span>
</mb-table-tooltip>
<mb-icon v-if="col.copyText" class="copy-text" icon="CopyOutline" @click="copyText(dictStore.getDictLabel(col.dictType, getValueByPath(row, col.field) + ''))" />
</template>
<template v-else-if="type === 'image'">
<n-image-group v-if="row[col.field]">
<n-space>
<n-image
v-for="it in row[col.field].split(',')"
:width="componentProperties.table.image.width"
:height="componentProperties.table.image.height"
:src="it && it.startsWith('http') ? it : $global.filePrefix + encodeURIComponent(it)"
/>
</n-space>
</n-image-group>
</template>
<template v-else>
<mb-table-tooltip :nowrap="nowrap">
<span v-html="getLabel(row, col)"></span>
</mb-table-tooltip>
<mb-icon v-if="col.copyText" class="copy-text" icon="CopyOutline" @click="copyText(getLabel(row, col))" />
</template>
</span>
</template>
<script setup>
import {get as getValueByPath} from "lodash-es";
import componentProperties from '@/components/magic-component-properties'
import {useDictStore} from "@/store/modules/dictStore";
const dictStore = useDictStore()
const props = defineProps({
type: {
type: String,
default: ''
},
row: {
type: Object,
default: () => {}
},
col: {
type: Object,
default: () => {}
},
index: {
type: Number,
default: undefined
},
nowrap: {
type: Boolean,
default: undefined
}
})
function copyText(text){
$common.copyText(text)
}
function getLabel(row, col){
return $common.notEmptyNot01(getValueByPath(row, col.field)) ? getValueByPath(row, col.field) : $common.notEmptyNot01(col.defaultValue) ? col.defaultValue : ''
}
</script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,68 @@
<template>
<n-tree-select
ref="magicTreeSelect"
v-model:value="selectValue"
:options="options"
:placeholder="placeholder || (itemLabel && '请选择' + itemLabel)"
:multiple="multiple"
:key-field="valueField"
:label-field="labelField"
v-bind="props.props"
default-expand-all
filterable
/>
</template>
<script setup>
import { ref,nextTick } from "vue"
import { watchValue } from '@/components/magic/scripts/watch-join-update.js'
import { getTreeSelectData } from '@/api/components/mb-tree-select.js'
const magicTreeSelect = ref()
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: {
type: String,
default: null
},
url: {
type: String,
default: '',
required: true
},
itemLabel: String,
placeholder: String,
props: Object,
multiple: {
type: Boolean,
default: false
},
join: {
type: Boolean,
default: true
},
labelField: {
type: String,
default: 'label'
},
valueField: {
type: String,
default: 'key'
}
})
const selectValue = ref(props.multiple ? [] : null)
const options = ref([])
getTreeSelectData(props).then(data => {
options.value = data
watchValue(selectValue, props, emit)
})
function expand(){
nextTick(() => magicTreeSelect.value.handleTriggerClick())
}
defineExpose({ expand })
</script>

View File

@ -0,0 +1,399 @@
<template>
<div class="h-full w-full" :style="style">
<div class="flex flex-col h-full w-full">
<div style="margin-bottom: 5px;" v-if="expand || checked">
<n-space>
<n-button :size="$global.uiSize.value" v-if="expand" type="primary" @click="doExpand">展开/折叠</n-button>
<n-button :size="$global.uiSize.value" v-if="checked" type="primary"
@click="() => { treeAllChecked = !treeAllChecked; checkedAll(treeAllChecked) }">全选/全不选
</n-button>
</n-space>
</div>
<div style="padding: 5px" v-if="search">
<n-input :size="$global.uiSize.value" v-model:value="searchValue" placeholder="输入关键字进行过滤"/>
</div>
<n-tree
class="flex-1"
v-bind="props.props"
virtual-scroll
block-line
:cascade="cascade"
:checkable="checkable"
:show-line="showLine"
:style="treeStyle"
key-field="id"
label-field="name"
:data="treeData"
:checked-keys="checkedKeys"
:pattern="searchValue"
:show-irrelevant-nodes="false"
:default-expand-all="defaultExpandAll"
:node-props="nodeProps"
@update:checked-keys="updateCheckedKeys"
@update:expanded-keys="updatePrefixWithExpaned"
/>
</div>
<n-dropdown
placement="bottom-start"
trigger="manual"
:show="showDropdown"
:options="contextmenu"
:x="dropdownX"
:y="dropdownY"
@select="dropdownSelect"
@clickoutside="() => showDropdown = false"
>
<!-- 需要有一个空的节点不然报错-->
<div></div>
</n-dropdown>
</div>
</template>
<script setup>
import {watch, ref, nextTick, onBeforeMount, h} from 'vue'
import treeTable from '@/scripts/treeTable'
import { clone, uniq, pull, pullAll, isEmpty } from 'lodash-es'
import MbIcon from "@/components/magic/basic/mb-icon.vue";
const emit = defineEmits(['update:modelValue', 'check-change', 'node-click'])
const props = defineProps({
url: {
type: String,
default: ''
},
params: {
type: Object,
default: () => {
}
},
modelValue: {
type: String,
default: ''
},
style: {
type: String,
default: ''
},
treeStyle: {
type: Object,
default: () => {
}
},
props: {
type: Object,
default: () => {
}
},
expand: {
type: Boolean,
default: true
},
checked: {
type: Boolean,
default: true
},
search: {
type: Boolean,
default: false
},
searchWidth: {
type: String,
default: '230px'
},
keyAll: {
type: Boolean,
default: true
},
cascade: {
type: Boolean,
default: true
},
checkable: {
type: Boolean,
default: false
},
showLine: {
type: Boolean,
default: false
},
contextmenu: {
type: Array,
default: undefined
},
icon: {
type: Object,
default: undefined
},
defaultExpandAll: {
type: Boolean,
default: true
}
})
const showDropdown = ref(false)
const dropdownX = ref()
const dropdownY = ref()
const checkedAllKeys = ref([])
const checkedKeys = ref([])
function updateCheckedKeys(keys, option, meta) {
if(!props.cascade){
checkedKeys.value = keys
}else{
let action = meta.action
let node = meta.node
let id = node.id
let pid = node.pid
let children = node.children
//
checkedAllKeys.value = uniq(checkedAllKeys.value)
if (isEmpty(children)) {
if (action == 'check') {
// ""
checkedKeys.value.push(id)
// """"
checkedAllKeys.value.push(...getParentIds(id))
checkedAllKeys.value.push(id)
} else {
// ""
pull(checkedKeys.value, id)
// """"
pull(checkedAllKeys.value, id)
upRecursionCheck(pid)
}
} else {
//
let selectedKeys = getIds(children)
//
let selectedAllKeys = getIds(children, true)
if (action == 'check') {
//
checkedKeys.value.push(...selectedKeys)
// """"""
checkedAllKeys.value.push(id)
checkedAllKeys.value.push(...getParentIds(id))
checkedAllKeys.value.push(...selectedAllKeys)
} else {
//
pullAll(checkedKeys.value, selectedKeys)
// """"""
pullAll(checkedAllKeys.value, selectedAllKeys)
pull(checkedAllKeys.value, id)
upRecursionCheck(pid)
}
}
//
checkedAllKeys.value = uniq(checkedAllKeys.value)
}
updateKeys()
}
function updatePrefixWithExpaned(_keys, _option, meta){
if(props.icon){
if (!meta.node)
return;
switch (meta.action) {
case "expand":
meta.node.prefix = () => h(MbIcon, { icon: props.icon.expand })
break;
case "collapse":
meta.node.prefix = () => h(MbIcon, { icon: props.icon.collapse })
break;
}
}
}
function upRecursionCheck(pid){
if(pid != '0'){
//
let siblings = sourceData.value.filter(it => it.pid == pid)
// id
if(!siblings.some(it => checkedAllKeys.value.indexOf(it.id) != -1)){
pull(checkedAllKeys.value, pid)
//
let parent = sourceData.value.filter(it => it.id == pid)[0]
upRecursionCheck(parent.pid)
}
}
}
function getIds(children, all) {
all = all == undefined ? false : true
let ids = []
getAllSubs(children, ids, all)
return ids
}
function getAllSubs(children, ids, all){
children.forEach(it => {
if (it.children && it.children.length > 0) {
if(all){
ids.push(it.id)
}
getAllSubs(it.children, ids, all)
}else{
ids.push(it.id)
}
})
}
// id
function getParentIds(id){
let ids = []
upRecursion(id, ids)
return ids
}
function upRecursion(id, ids){
let menu = sourceData.value.filter(it => it.id == id)[0]
if(menu && menu.pid != '0'){
ids.push(menu.pid)
let parentMenu = sourceData.value.filter(it => it.id == menu.pid)[0]
if(parentMenu){
upRecursion(parentMenu.id, ids)
}
}
}
function loadSourceData(children){
children.forEach(it => {
let chi = clone(it)
delete chi.children
sourceData.value.push(chi)
if(it.children && it.children.length > 0){
loadSourceData(it.children)
}
})
}
const tree = ref()
const treeData = ref([])
const sourceData = ref([])
const defaultExpandAll = ref(props.defaultExpandAll)
const refreshTree = ref(false)
const treeAllChecked = ref(false)
const searchValue = ref('')
onBeforeMount(() => {
loadTreeData()
})
function updateKeys(){
//
if(props.cascade && props.keyAll){
emit('update:modelValue', checkedAllKeys.value.join(','))
emit('check-change', checkedAllKeys.value.join(','))
}else{
emit('update:modelValue', checkedKeys.value.join(','))
emit('check-change', checkedKeys.value.join(','))
}
}
function selectIds(value) {
if(value){
let ids = []
let values = value.split(',')
checkedAllKeys.value = values
values.forEach(id => {
// id
if(!sourceData.value.some(it => it.pid == id)){
ids.push(id)
}
})
if(props.cascade){
checkedKeys.value = ids
}else{
checkedKeys.value = values
}
}
}
function doExpand() {
refreshTree.value = false
defaultExpandAll.value = !defaultExpandAll.value
nextTick(() => refreshTree.value = true)
}
const currentNode = ref()
function nodeProps({option}) {
return {
onClick() {
emit('node-click', option)
},
onContextmenu(e) {
currentNode.value = option
props.contextmenu.forEach(it => {
it.show = (it.if && it.if(currentNode.value))
})
showDropdown.value = true;
dropdownX.value = e.clientX;
dropdownY.value = e.clientY;
e.preventDefault();
}
}
}
function recursionRenderIcon(children){
children.forEach(it => {
if(it.isGroup){
if(it.children && it.children.length){
it.prefix = () => h(MbIcon, { icon: defaultExpandAll.value ? props.icon.expand : props.icon.collapse })
recursionRenderIcon(it.children)
}else{
it.prefix = () => h(MbIcon, { icon: props.icon.collapse })
}
}else{
it.prefix = () => h(MbIcon, { icon: props.icon.node, color: '#42b883' })
}
})
}
function loadTreeData() {
$common.get(props.url, props.params).then((res) => {
treeTable.deleteEmptyChildren(res.data.list)
if(props.icon){
recursionRenderIcon(res.data.list)
}
treeData.value = res.data.list
loadSourceData(treeData.value)
refreshTree.value = true
nextTick(() => selectIds(props.modelValue))
watch(() => props.modelValue, (value) => {
nextTick(() => selectIds(value))
})
})
}
function reload(){
loadTreeData()
}
function getTree() {
return tree.value
}
function checkedAll(checked) {
if (checked) {
checkedKeys.value = getIds(treeData.value)
checkedAllKeys.value = sourceData.value.map(it => it.id)
} else {
checkedKeys.value = []
checkedAllKeys.value = []
}
updateKeys()
}
function dropdownSelect(key){
showDropdown.value = false
props.contextmenu && props.contextmenu.filter(it => it.key == key)[0].click(currentNode.value)
}
defineExpose({getTree, reload})
</script>

View File

@ -0,0 +1,131 @@
<template>
<lay-layer v-model="$global.modal.modalMap[modalId].value" v-bind="layerOptions">
<div style="padding: 20px">
<slot />
</div>
</lay-layer>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { layer } from '@layui/layer-vue';
const modalId = $common.uuid()
$global.modal.create(modalId);
const emit = defineEmits(['confirm'])
const props = defineProps({
title: {
type: String,
default: ''
},
width: {
type: String,
default: 'auto'
},
height: {
type: String,
default: 'auto'
},
shadeClose: {
type: Boolean,
default: false
},
shade: {
type: Boolean,
default: true
},
resize: {
type: Boolean,
default: true
},
maxmin: {
type: Boolean,
default: true
},
shadeOpacity: {
type: String,
default: '.4'
},
showFooter: {
type: Boolean,
default: true
},
buttons: {
type: Array,
default: undefined
}
})
const layerOptions = reactive({
title: props.title,
zIndex: $global.modal.getIndex(modalId),
shade: props.shade,
resize: props.resize,
maxmin: props.maxmin,
shadeOpacity: props.shadeOpacity,
shadeClose: props.shadeClose,
area: [props.width, props.height],
btn: !props.showFooter ? [] : props.buttons !== undefined ? props.buttons : [
{
text: "确定",
callback: () => {
confirm()
}
},
{
text: "取消",
callback: () => {
hide()
}
}
]
})
watch(() => props.title, (value) => {
layerOptions.title = value
})
const confirmLoading = ref(false)
function show() {
$global.modal.show(modalId)
}
function hide() {
$global.modal.hide(modalId)
}
function loading() {
confirmLoading.value = layer.load(1)
}
function hideLoading() {
layer.close(confirmLoading.value)
}
function confirm() {
emit('confirm', {
loading,
hideLoading,
hide,
title: props.title
})
}
defineExpose({show, hide, loading, hideLoading})
</script>
<style>
.layui-layer-title{
font-size: 16px;
}
.layui-layer-btn .layui-layer-btn0 {
background-color: #2D8CF0FF;
border-color: #2D8CF0FF;
}
.layui-layer-setwin i:hover {
color: #2D8CF0FF
}
.layui-layer{
max-height: 100%;
max-width: 100%;
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<span>
<span v-if="getNowrap" @mouseover="onMouseover">
<n-ellipsis :tooltip="tooltip">
<slot />
</n-ellipsis>
</span>
<span v-else>
<slot />
</span>
</span>
</template>
<script setup>
import {computed, ref} from 'vue'
import componentProperties from "@/components/magic-component-properties.js";
const props = defineProps({
nowrap: {
type: Boolean,
default: true
}
})
const getNowrap = computed(() => props.nowrap !== undefined ? props.nowrap : componentProperties.table.nowrap !== undefined ? componentProperties.table.nowrap : false)
const tooltip = ref()
tooltip.value = false
let timers
let duration
function onMouseover(){
clearTimeout(timers)
const onUpdateShow = (value) => {
if (!value) {
timers = setTimeout(() => {
tooltip.value = false
}, (duration ?? 100) + 1000)
}
}
tooltip.value = { onUpdateShow }
}
</script>

View File

@ -0,0 +1,112 @@
<template>
<n-checkbox-group ref="magicCheckbox" v-model:value="checkboxValue">
<n-space item-style="display: flex;">
<n-checkbox v-for="it in options" :value="it.value" :label="it.label" :key="it.value" />
</n-space>
</n-checkbox-group>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { getCheckboxData } from '@/api/components/mb-checkbox.js'
import { watchValue } from "@/components/magic/scripts/watch-join-update";
import { isArray } from "lodash-es";
const magicCheckbox = ref()
const emit = defineEmits(['update:modelValue', 'change'])
const props = defineProps({
modelValue: {
required: true
},
type: {
type: String,
default: ''
},
options: {
type: Array,
default: () => []
},
optionsFilter: {
type: Function,
default: undefined
},
url: {
type: String,
default: ''
},
params: {
type: Object,
default: () => {
}
},
labelField: {
type: String,
default: 'label'
},
valueField: {
type: String,
default: 'value'
},
props: {
type: Object,
default: () => {
}
},
join: {
type: Boolean,
default: true
},
defaultFirstItem: {
type: Boolean,
default: false
},
done: {
type: Function,
default: () => {}
},
multiple: {
type: Boolean,
default: true
}
})
const options = ref([])
const checkboxValue = ref([])
watch(() => [props.type, props.url, props.options], () => {
loadData()
}, {deep: true})
onMounted(() => {
loadData()
})
let watchList = []
function loadData() {
getCheckboxData({
type: props.type,
url: props.url,
params: props.params,
options: props.options,
optionsFilter: props.optionsFilter,
labelField: props.labelField,
valueField: props.valueField
}).then(data => {
options.value = data
props.done(data)
$common.stopWatchList(watchList)
watchList = watchValue(checkboxValue, props, emit)
if(props.defaultFirstItem && options.value && options.value[0]){
let defaultValue = options.value[0].value
checkboxValue.value = isArray(checkboxValue.value) ? [defaultValue] : defaultValue
}
})
}
function getOptions(){
return options.value
}
defineExpose({ getOptions })
</script>

View File

@ -0,0 +1,73 @@
<template>
<n-date-picker
:size="$global.uiSize.value"
v-model:formatted-value="selectValue"
:type="type"
:format="valueFormat"
:value-format="valueFormat"
:placeholder="placeholder"
:start-placeholder="startPlaceholder"
:end-placeholder="endPlaceholder"
update-value-on-close
v-bind="props.props"
/>
</template>
<script setup>
import {watch, ref} from 'vue'
const emit = defineEmits(['update:modelValue'])
const selectValue = ref('')
const props = defineProps({
modelValue: String,
type: {
type: String,
default: 'date'
},
placeholder: {
type: String,
default: '请选择时间'
},
format: {
type: String,
default: ''
},
startPlaceholder: {
type: String,
default: '开始时间'
},
endPlaceholder: {
type: String,
default: '结束时间'
},
props: Object
})
function handlerValue(value){
if(value && value.indexOf(',') !== -1){
return value.split(',')
}
return value
}
selectValue.value = handlerValue(props.modelValue)
watch(() => props.modelValue, (value) => {
if(!$common.arrayStringEq(value, selectValue.value)){
selectValue.value = handlerValue(value)
}
})
const valueFormat = ref()
if (!props.format) {
if (props.type.startsWith('date')) {
valueFormat.value = 'yyyy-MM-dd'
}
if (props.type.startsWith('datetime')) {
valueFormat.value = 'yyyy-MM-dd HH:mm:ss'
}
} else {
valueFormat.value = props.format
}
watch(selectValue, (value) => {
emit('update:modelValue', value instanceof Array ? value.join(',') : value)
})
</script>

View File

@ -0,0 +1,23 @@
<template>
<n-input
ref="magicInput"
:size="$global.uiSize.value"
v-model:value="selectValue"
:type="type"
:placeholder="placeholder || (itemLabel && '请输入' + itemLabel)"
v-bind="props.props"
/>
</template>
<script setup>
import { useVModel } from "@vueuse/core";
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: String,
itemLabel: String,
placeholder: String,
type: String,
props: Object
})
const selectValue = useVModel(props, 'modelValue', emit)
</script>

View File

@ -0,0 +1,206 @@
<style scoped>
.mb-list{
border-radius: 5px;
border: 1px solid #ccc;
background: white;
padding: 10px;
}
</style>
<template>
<div class="list-container" ref="listContainerRef">
<mb-input ref="magicInput" @click="inputClick" v-model="inputValue" />
<div class="mb-list" :style="{ width: width + 'px', height: height + 'px', ...componentStyle }" v-if="showList">
<div class="mb-search" v-if="!search">
<mb-search :where="selectTableOptions.where" @search="reloadTable" />
</div>
<div class="mb-toolbar">
<n-button v-if="multiple" :size="$global.uiSize.value" type="primary" @click="selectDataList">
选择数据
</n-button>
</div>
<div class="mb-table">
<mb-table
ref="magicTable"
v-bind="selectTableOptions"
v-model:checked-row-keys="checkedRowKeys"
@dblclick="tableDblclick"
/>
</div>
</div>
</div>
</template>
<script setup>
import { onBeforeUnmount, ref, watch, toRaw } from 'vue'
import { onClickOutside } from '@vueuse/core'
import { clone, cloneDeep } from 'lodash-es'
const magicTable = ref()
const magicInput = ref()
const listContainerRef = ref()
const checkedRowKeys = ref()
const showList = ref(false)
const props = defineProps({
modelValue: {
required: true
},
width: {
type: Number,
default: 500
},
height: {
type: Number,
default: 300
},
tableOptions: {
type: Object,
default: () => {}
},
onSelectData: {
type: Function,
default: () => {}
},
multiple: {
type: Boolean,
default: false
},
closeCurrentColEditMode: {
type: Function,
default: undefined
},
search: {
type: Object,
default: undefined
}
})
const selectTableOptions = ref(cloneDeep(props.tableOptions))
const sourceData = ref()
selectTableOptions.value.done = (data) => {
sourceData.value = data
}
initTableOptions()
watch(() => props.tableOptions, (value) => {
selectTableOptions.value = value
initTableOptions()
}, { deep: true })
function initTableOptions(){
selectTableOptions.value.selection = props.multiple
}
const inputValue = ref(clone(props.modelValue))
if(props.search){
watch(inputValue, (value) => {
let fields = props.search.fields
if(props.search.static){
selectTableOptions.value.data = sourceData.value.filter((it) => {
for(let i = 0; i<fields.length; i++){
if(it[fields[i]].indexOf(value) != -1){
return true
}
}
return false
})
}else{
selectTableOptions.value.where = selectTableOptions.value.where || {}
fields.forEach(field => {
selectTableOptions.value.where[field] = value
})
}
})
watch(() => selectTableOptions.value.where, () => reloadTable(), { deep: true })
}
watch(() => props.modelValue, (value) => {
inputValue.value = value
})
watch(() => listContainerRef.value, () => {
addEventListener()
onClickOutside(listContainerRef, () => closeTable())
})
function tableDblclick({ row }){
props.onSelectData({
selectData: row,
multiple: false,
_deconstruction_: true
})
closeTable()
}
function selectDataList(){
let rowKey = selectTableOptions.value['rowKey'] || 'id'
props.onSelectData({
selectData: magicTable.value.getData().filter(it => checkedRowKeys.value.indexOf(it[rowKey]) != -1),
multiple: true,
_deconstruction_: true
})
closeTable()
}
const componentStyle = ref()
function inputClick(){
let style = {}
let inputRect = magicInput.value.$el.getBoundingClientRect()
let bodyClientWidth = document.body.clientWidth
let bodyClientHeight = document.body.clientHeight
let top = inputRect.y + inputRect.height
let left = inputRect.x
style.position = 'fixed'
style['z-index'] = 999999
if((bodyClientWidth - props.width - left) < 1){
style.right = `1px`
style.left = 'unset'
}else{
style.left = `${left}px`
style.right = 'unset'
}
if((bodyClientHeight - props.height - top) < 1){
style.bottom = `${bodyClientHeight - inputRect.y}px`
style.top = 'unset'
}else{
style.top = `${top}px`
style.bottom = 'unset'
}
componentStyle.value = style
showList.value = true
}
function reloadTable() {
magicTable.value && magicTable.value.reload()
}
function keydown(e){
// esc
if(e && e.keyCode == 27){
closeTable()
}
}
function closeTable(){
props.closeCurrentColEditMode && props.closeCurrentColEditMode()
showList.value = false
}
function addEventListener() {
listContainerRef.value.addEventListener('keydown', keydown)
}
function removeListener() {
listContainerRef.value.removeEventListener('keydown', keydown)
}
onBeforeUnmount(() => {
removeListener()
})
function focus(){
magicInput.value.$refs[Object.keys(toRaw(magicInput.value.$refs))[0]].focus()
inputClick()
}
defineExpose({ focus })
</script>

View File

@ -0,0 +1,143 @@
<template>
<n-select
ref="magicSelect"
:size="$global.uiSize.value"
v-bind="props.props"
v-model:value="selectValue"
:multiple="multiple"
:options="options"
:style="{ width }"
:placeholder="placeholder || (itemLabel && '请输入' + itemLabel)"
:clearable="clearable"
:show-on-focus="showOnFocus"
filterable
max-tag-count="responsive"
/>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { getSelectData } from '@/api/components/mb-select.js'
import { watchValue } from "@/components/magic/scripts/watch-join-update";
import { isArray } from "lodash-es";
const magicSelect = ref()
const emit = defineEmits(['update:modelValue', 'change'])
const props = defineProps({
modelValue: {
required: true
},
type: {
type: String,
default: ''
},
options: {
type: Array,
default: () => []
},
optionsFilter: {
type: Function,
default: undefined
},
url: {
type: String,
default: ''
},
params: {
type: Object,
default: () => {
}
},
labelField: {
type: String,
default: 'label'
},
valueField: {
type: String,
default: 'value'
},
props: {
type: Object,
default: () => {
}
},
width: {
type: String,
default: '100%'
},
allOption: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: ''
},
itemLabel: String,
multiple: {
type: Boolean,
default: false
},
join: {
type: Boolean,
default: true
},
clearable: {
type: Boolean,
default: true
},
showOnFocus: {
type: Boolean,
default: true
},
defaultFirstItem: {
type: Boolean,
default: false
},
done: {
type: Function,
default: () => {}
}
})
const options = ref([])
const selectValue = ref(props.multiple ? [] : null)
watch(() => [props.type, props.url, props.options], () => {
loadData()
}, {deep: true})
onMounted(() => {
loadData()
})
let watchList = []
function loadData() {
getSelectData({
type: props.type,
url: props.url,
params: props.params,
options: props.options,
optionsFilter: props.optionsFilter,
allOption: props.allOption,
labelField: props.labelField,
valueField: props.valueField
}).then(data => {
options.value = data
props.done(data)
$common.stopWatchList(watchList)
watchList = watchValue(selectValue, props, emit)
if(props.defaultFirstItem && options.value && options.value[0]){
let defaultValue = options.value[0].value
selectValue.value = isArray(selectValue.value) ? [defaultValue] : defaultValue
}
})
}
function getOptions(){
return options.value
}
defineExpose({ getOptions })
</script>

View File

@ -0,0 +1,81 @@
<template>
<n-switch
ref="magicSwitch"
:size="$global.uiSize.value"
v-model:value="selectValue"
:checked-value="_checkedValue"
:unchecked-value="_uncheckedValue"
v-bind="props.props"
@update:value="change"
>
<template #checked>
<slot name="checked"></slot>
</template>
<template #unchecked>
<slot name="unchecked"></slot>
</template>
</n-switch>
</template>
<script setup>
import {ref, watch} from 'vue'
const emit = defineEmits(['update:modelValue', 'change'])
const selectValue = ref('')
const props = defineProps({
modelValue: Boolean | String | Number,
checkedValue: Boolean | String | Number,
uncheckedValue: Boolean | String | Number,
props: Object
})
const _checkedValue = ref(true)
const _uncheckedValue = ref(false)
function change() {
emit('update:modelValue', selectValue.value)
emit('change', selectValue.value)
}
function setActive(value) {
if($common.notEmptyNot01(value)){
if (typeof (value) == 'boolean') {
_checkedValue.value = true
_uncheckedValue.value = false
} else {
if (props.checkedValue == undefined && props.uncheckedValue == undefined) {
_checkedValue.value = '1'
_uncheckedValue.value = '0'
} else {
_checkedValue.value = props.checkedValue + ''
_uncheckedValue.value = props.uncheckedValue + ''
}
}
}else{
_checkedValue.value = props.checkedValue || true
_uncheckedValue.value = props.uncheckedValue || false
}
}
dynamicSetValue(props.modelValue)
setActive(props.modelValue)
watch(() => props.modelValue, (value) => {
dynamicSetValue(value)
setActive(value)
})
function dynamicSetValue(value) {
if (typeof (value) == 'boolean') {
selectValue.value = value
} else {
if (value || value == 0) {
selectValue.value = value + ''
} else {
selectValue.value = false
}
}
}
watch(selectValue, (value) => {
emit('update:modelValue', value)
})
</script>

View File

@ -0,0 +1,24 @@
<template>
<n-input
ref="magicTextarea"
:size="$global.uiSize.value"
v-model:value="selectValue"
type="textarea"
:placeholder="placeholder || (itemLabel && '请输入' + itemLabel)"
v-bind="props.props"
:rows="rows"
/>
</template>
<script setup>
import { useVModel } from "@vueuse/core";
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: String,
props: Object,
itemLabel: String,
placeholder: String,
rows: Number
})
const selectValue = useVModel(props, 'modelValue', emit)
</script>

View File

@ -0,0 +1,372 @@
<template>
<n-upload
:id="uploadDomId"
class="upload-demo"
ref="uploadRef"
:action="actionUrl"
:headers="headers"
:directory-dnd="directoryDnd"
@preview="onPreview"
@remove="onRemove"
:multiple="multiple"
:limit="limit"
:show-file-list="showFileList"
@before-upload="beforeUpload"
@finish="onFinish"
@change="onChange"
@error="onError"
:file-list="fileList"
:default-upload="defaultUpload"
>
<n-upload-dragger v-if="directoryDnd">
<div>可拖拽上传</div>
<n-button type="primary" :loading="uploadLoading" :disabled="!multiple && fileList.length == 1">
{{
label
}}
</n-button>
<div slot="tip" v-if="showTip" class="el-upload__tip">支持上传
{{
getSettingSuffixs().replaceAll(',', '')
}}文件且不超过{{ maxFileSize }}MB
</div>
</n-upload-dragger>
<template v-else>
<n-button type="primary" :loading="uploadLoading" :disabled="!multiple && fileList.length == 1">
{{
label
}}
</n-button>
<div slot="tip" v-if="showTip" class="el-upload__tip">支持上传
{{
getSettingSuffixs().replaceAll(',', '')
}}文件且不超过{{ maxFileSize }}MB
</div>
</template>
</n-upload>
</template>
<script setup>
import {ref, watch, onMounted} from 'vue'
import {useUserStore} from "@/store/modules/userStore";
import global from '@/scripts/global'
const userStore = useUserStore()
const emit = defineEmits(['change', 'update:modelValue'])
const props = defineProps({
modelValue: {
required: false
},
multiple: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 20
},
maxFileSize: {
type: Number,
default: 200
},
accept: {
type: String,
default: ''
},
externalId: {
type: String,
default: ''
},
externalType: {
type: String,
default: ''
},
formats: {
type: String,
default: ''
},
label: {
type: String,
default: '点击上传'
},
showTip: {
type: Boolean,
default: () => true
},
action: {
type: String,
default: ''
},
showFileList: {
type: Boolean,
default: () => true
},
onSuccess: {
type: Function,
default: () => {
}
},
deleteTip: {
type: Boolean,
default: () => true
},
join: {
type: Boolean,
default: true
},
directoryDnd: {
type: Boolean,
default: false
},
defaultUpload: {
type: Boolean,
default: true
}
})
const acceptList = {
image: 'png,jpg,gif,jpeg',
wps: 'pdf,pptx,xls,xlsx,csv,docx,doc',
compress: 'zip,rar,7z',
video: 'avi,flv,mp4,mpeg,mov'
}
const actionUrl = ref(global.baseApi + '/system/file/upload')
const headers = {
token: userStore.getToken()
}
const uploadRef = ref()
const urls = ref([])
const uploadDomId = $common.uuid()
const fileList = ref([])
const uploadLoading = ref(false)
const emitUpdate = ref(true)
watch(() => props.modelValue, () => {
if (emitUpdate.value) {
emitUpdate.value = false
if (fileList.value.length == 0) {
renderFile()
}
} else {
renderFile()
}
})
onMounted(() => {
if (props.externalId) {
$common.get('/system/file/files', {
externalId: props.externalId,
externalType: props.externalType
}).then(res => {
const {data} = res
fileList.value = data
})
actionUrl.value = actionUrl.value + `?externalId=${props.externalId}&externalType=${props.externalType}`
} else {
renderFile()
}
if (props.action) {
actionUrl.value = global.baseApi + props.action
}
})
function setFileList() {
if (urls.value.length > 0) {
fileList.value = urls.value.map(it => {
return {
name: it.substring(it.lastIndexOf('/') + 1),
fullPath: it
}
})
}
}
function renderFile() {
if (props.multiple && props.join && props.modelValue) {
urls.value = props.modelValue.split(',')
} else {
if (props.modelValue instanceof Array && props.modelValue.length > 0) {
urls.value = props.modelValue
} else {
if (props.modelValue) {
urls.value = [props.modelValue]
}
}
}
setFileList()
}
function onChange(data) {
if (data.file.status != "removed") {
fileList.value = data.fileList
}
}
function onError() {
uploadLoading.value = false
}
function updateValue(value) {
emit('update:modelValue', value)
emitUpdate.value = true
emit('change', value)
}
function onRemove({file}) {
let deleteFile = () => {
let url = file.fullPath
urls.value.splice(urls.value.indexOf(url), 1)
fileList.value.forEach((it, i) => {
if (it && url == it.fullPath) {
fileList.value.splice(i, 1)
}
})
if (props.multiple) {
if (props.join) {
updateValue(urls.value.join(','))
} else {
updateValue(urls.value)
}
} else {
document.getElementById(uploadDomId).getElementsByClassName('n-upload-file-input')[0].removeAttribute('disabled')
updateValue('')
}
$common.delete('/system/file/delete', {url: encodeURI(url)})
}
return new Promise((resolve, reject) => {
if (!props.deleteTip) {
deleteFile()
resolve()
} else {
$dialog.warning({
title: '提示',
content: '确定要删除此文件吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
deleteFile()
resolve()
},
onClose: () => {
reject()
}
})
}
})
}
function onPreview(file) {
window.open(global.baseApi + file.fullPath)
}
function onFinish({file, event}) {
let res = JSON.parse((event?.target).response)
file.fullPath = res.data.url
uploadLoading.value = false
if (res.data) {
urls.value.push(res.data.url)
if (props.multiple) {
if (props.join) {
updateValue(urls.value.join(','))
} else {
updateValue(urls.value)
}
} else {
document.getElementById(uploadDomId).getElementsByClassName('n-upload-file-input')[0].setAttribute('disabled', '')
updateValue(res.data.url)
}
}
if (props.onSuccess) {
props.onSuccess({file, event})
}
if (props.action) {
urls.value = []
fileList.value = []
updateValue(urls.value.join(','))
}
return file
}
function getSettingSuffixs() {
if (props.formats) {
return props.formats
}
let suffixs = acceptList[props.accept]
if (!suffixs) {
suffixs = getAllSuffixs()
}
return suffixs
}
function beforeUpload({file}) {
let fileName = file.name
let accepts = props.accept.split(',')
if (accepts) {
for (let i = 0; i < accepts.length; i++) {
if (!validAccept(fileName, accepts[i])) {
$message.error('上传文件格式只能为:' + getSettingSuffixs().replaceAll(',', ''))
return false
}
}
} else {
if (!validAccept(fileName, 'null')) {
$message.error('上传文件格式只能为:' + getAllSuffixs().replaceAll(',', ''))
return false
}
}
const isLt2M = file.file.size / 1024 / 1024 < props.maxFileSize
if (!isLt2M) {
$message.error(`上传文件大小不能超过 ${props.maxFileSize}MB`)
return isLt2M
}
if (props.defaultUpload) {
uploadLoading.value = true
}
}
function getAllSuffixs() {
let suffixs = ''
for (const key in acceptList) {
suffixs += acceptList[key] + ','
}
suffixs = suffixs.substring(0, suffixs.length - 1)
return suffixs
}
function validAccept(fileName, accept) {
if (props.formats) {
return validEndsWith(fileName, props.formats)
}
if (accept && acceptList[accept]) {
return validEndsWith(fileName, acceptList[accept])
} else {
return validEndsWith(fileName, getAllSuffixs())
}
}
function validEndsWith(fileName, suffixs) {
suffixs = suffixs.split(',')
for (let i = 0; i < suffixs.length; i++) {
const suffix = suffixs[i]
if (fileName.toLowerCase().endsWith('.' + suffix)) {
return true
}
}
return false
}
function getFileList() {
return fileList.value
}
defineExpose({getFileList})
</script>
<style scoped>
:deep(.n-upload) {
display: block;
}
</style>

View File

@ -0,0 +1,423 @@
<template>
<div>
<n-image-group>
<draggable
v-model="urls"
class="vue-draggable"
tag="div"
draggable=".draggable-item"
@end="onDragEnd"
item-key="id"
>
<template #item="{ element }">
<div class="draggable-item" :style="{ width: width + 'px', height: height + 'px' }">
<n-image :src="$global.baseApi + element"/>
<div class="tools">
<div class="shadow" @click="handleDelete(element)">
<n-icon size="20">
<Trash/>
</n-icon>
</div>
<div class="shadow" @click="beforeCropper(element)">
<n-icon size="20">
<Crop/>
</n-icon>
</div>
</div>
</div>
</template>
<template #footer>
<n-upload
v-if="(!multiple && urls.length == 0) || (multiple && urls.length < limit)"
class="uploadBox"
:style="{ width: width + 'px', height: height + 'px' }"
:action="action"
:headers="headers"
accept=".jpg,.jpeg,.png,.gif"
directory-dnd
:show-file-list="false"
:multiple="multiple"
:max="limit"
@change="onChange"
@finish="onFinish"
@error="onError"
@before-upload="beforeUpload"
:file-list="fileList"
:disabled="disabled"
>
<n-upload-dragger>
<n-icon size="30" class="uploadIcon">
<span class="draggable-text">可拖拽上传</span>
<Add/>
<span v-show="isUploading" class="uploading">正在上传...</span>
<span v-if="!isUploading && limit && limit!==99 && multiple"
class="limitTxt">最多{{ limit }}</span>
</n-icon>
</n-upload-dragger>
</n-upload>
</template>
</draggable>
</n-image-group>
<div v-if="tip" :style="{ color: tipColor }">{{ tip }}</div>
<mb-modal ref="cropperDialog" @confirm="cropper">
<div class="cropper-content">
<div class="cropper" style="text-align:center">
<vueCropper
ref="cropperRef"
v-bind="cropperOption"
:outputSize="cropperOption.outputSize === undefined ? 0.8 : cropperOption.outputSize"
:outputType="cropperOption.outputType === undefined ? 'jpeg' : cropperOption.outputType"
:canMove="cropperOption.canMove === undefined ? true : cropperOption.canMove"
:canMoveBox="cropperOption.canMoveBox === undefined ? true : cropperOption.canMoveBox"
:autoCrop="cropperOption.autoCrop === undefined ? true : cropperOption.autoCrop"
:centerBox="cropperOption.centerBox === undefined ? true : cropperOption.centerBox"
/>
</div>
</div>
</mb-modal>
</div>
</template>
<script setup>
import 'vue-cropper/dist/index.css'
import {ref, watch, onMounted} from 'vue'
import {VueCropper} from 'vue-cropper'
import draggable from 'vuedraggable'
import {Trash, Crop, Add} from "@vicons/ionicons5";
import {useUserStore} from "@/store/modules/userStore";
import global from '@/scripts/global'
import request from '@/scripts/request'
const emit = defineEmits(['update:modelValue', 'change'])
const props = defineProps({
modelValue: {
required: false
},
externalId: {
type: String,
default: ''
},
externalType: {
type: String,
default: ''
},
multiple: {
type: Boolean,
default: false
},
limit: {
type: Number,
default: 2
},
cropperConfig: {
type: Object,
default: () => {
}
},
width: {
type: Number,
default: 100
},
height: {
type: Number,
default: 100
},
tip: {
type: String,
default: ''
},
tipColor: {
type: String,
default: ''
},
deleteTip: {
type: Boolean,
default: () => true
},
join: {
type: Boolean,
default: true
}
})
const userStore = useUserStore()
const action = ref(global.baseApi + '/system/file/upload')
const headers = {token: userStore.getToken()}
const disabled = ref(false)
const isUploading = ref(false)
const cropperOption = ref({})
const urls = ref([])
const fileList = ref([])
const emitUpdate = ref(true)
const cropperDialog = ref()
const cropperRef = ref()
watch(() => props.modelValue, () => {
if (emitUpdate.value) {
emitUpdate.value = false
if (fileList.value.length == 0) {
renderFile()
}
} else {
renderFile()
}
})
onMounted(() => {
cropperOption.value = props.cropperConfig || {}
cropperOption.value.img = ''
if (props.externalId) {
$common.get('/system/file/files', {
externalId: props.externalId,
externalType: props.externalType
}).then(res => {
urls.value = res.data
})
action.value = action.value + `?externalId=${props.externalId}&externalType=${props.externalType}`
} else {
renderFile()
}
})
function renderFile() {
if (props.multiple && props.join && props.modelValue) {
urls.value = props.modelValue.split(',')
} else {
if (props.modelValue instanceof Array) {
urls.value = props.modelValue
fileList.value = urls.value.map(it => {
return {fullPath: it}
})
} else {
if (props.modelValue) {
urls.value.push(props.modelValue)
}
}
}
}
function updateValue(value) {
emit('update:modelValue', value)
emitUpdate.value = true
emit('change', value)
}
function handleDelete(url) {
let deleteFile = () => {
urls.value = urls.value.filter(it => it != url)
fileList.value = fileList.value.filter(it => it.fullPath != url)
$common.delete('/system/file/delete', {url: encodeURI(url)})
if (props.multiple) {
if (props.join) {
updateValue(urls.value.join(','))
} else {
updateValue(urls.value)
}
} else {
updateValue('')
}
}
if (!props.deleteTip) {
deleteFile()
} else {
$dialog.warning({
title: '提示',
content: '确定要删除此图片吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
deleteFile()
}
})
}
}
function onChange(data) {
fileList.value = data.fileList
}
function beforeUpload() {
isUploading.value = true
}
function onError() {
isUploading.value = false
}
function onFinish({file, event}) {
let res = JSON.parse((event?.target).response)
file.fullPath = res.data.url
urls.value.push(res.data.url)
if (props.multiple) {
if (props.join) {
updateValue(urls.value.join(','))
} else {
updateValue(urls.value)
}
} else {
updateValue(res.data.url)
}
onDragEnd()
isUploading.value = false
return file
}
function onDragEnd() {
$common.get('/system/file/resort', {urls: urls.value.map(url => encodeURI(url)).join(',')})
}
function beforeCropper(url) {
cropperOption.value.img = global.baseApi + url
cropperOption.value.relativeImg = url
cropperDialog.value.show()
}
function cropper() {
let relativeImg = cropperOption.value.relativeImg
cropperRef.value.getCropBlob((data) => {
let dataFile = new File([data], relativeImg.substring(relativeImg.lastIndexOf('/') + 1), {
type: data.type,
lastModified: Date.now()
})
let formData = new FormData()
formData.append('file', dataFile)
formData.append('url', encodeURI(relativeImg))
request({
url: '/system/file/cropper',
method: 'post',
data: formData
}).then(res => {
fileList.value[fileList.value.map(fl => fl.fullPath).indexOf(relativeImg)].fullPath = res.data.url
urls.value[urls.value.indexOf(relativeImg)] = res.data.url
cropperDialog.value.hide()
})
})
}
</script>
<style lang="less" scoped>
.uploadIcon {
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: #fbfdff;
border-radius: 6px;
font-size: 20px;
color: #999;
.limitTxt,
.uploading {
position: absolute;
bottom: 10%;
left: 0;
width: 100%;
font-size: 14px;
text-align: center;
}
.draggable-text {
position: absolute;
top: 10%;
left: 0;
width: 100%;
font-size: 14px;
text-align: center;
}
}
.vue-draggable {
display: flex;
flex-wrap: wrap;
.draggable-item {
margin-right: 5px;
margin-bottom: 5px;
box-sizing: border-box;
border: 1px solid #ddd;
border-radius: 6px;
position: relative;
overflow: hidden;
:deep(.n-image), :deep(.n-image img) {
width: 100%;
height: 100%;
}
:deep(.n-upload) {
width: 100%;
height: 100%;
display: block;
}
.tools {
position: absolute;
top: 0px;
width: 100%;
height: 20px;
}
.shadow {
display: inline-block;
background-color: rgba(0, 0, 0, .5);
opacity: 0;
transition: opacity .3s;
color: #fff;
font-size: 20px;
line-height: 20px;
padding: 2px;
cursor: pointer;
}
&:hover {
.shadow {
opacity: 1;
}
}
}
&.hideShadow {
.shadow {
display: none;
}
}
&.single {
overflow: hidden;
position: relative;
.draggable-item {
position: absolute;
left: 0;
top: 0;
z-index: 1;
}
}
&.maxHidden {
.uploadBox {
display: none;
}
}
}
.cropper-content {
.cropper {
width: auto;
height: 300px;
}
}
:deep(.n-upload-trigger) {
width: 100%;
height: 100%;
}
:deep(.n-upload-dragger) {
height: 100%;
padding: 0px;
}
</style>

View File

@ -0,0 +1,303 @@
<style scoped>
ul {
list-style: none;
font-size: 14px;
width: 100%;
overflow: auto;
}
:deep(.el-progress__text) {
min-width: 20px;
}
li {
padding: 10px;
border-radius: 5px;
min-height: 40px;
}
li:hover {
background: #F5F7FA;
}
.progress {
display: inline-block;
width: calc(100% - 15px);
}
.delete {
display: inline-block;
vertical-align: middle;
margin-top: 3px;
cursor: pointer;
}
.file-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
<template>
<div>
<div v-if="!readonly">
<n-button @click="btnClick">点击上传</n-button>
<div slot="tip" class="el-upload__tip">支持上传{{ getSettingSuffixs().replaceAll(',', '') }}文件</div>
</div>
<div style="font-size: 16px; font-weight: bold" v-if="readonly && (!fileList || fileList.length == 0)">
没有文件
</div>
<input ref="fileInput" type="file" multiple @change="fileChange" :accept="accept" style="display: none"/>
<ul :style="{ width, height }">
<li v-for="file in fileList" :title="file.name">
<div class="file-name">
{{ file.name }}
</div>
<div class="progress">
<n-progress :percentage="file.progress" :status="file.progress == 100 ? 'success' : ''"/>
</div>
<div class="delete" v-if="file.progress == 100 && !readonly">
<n-icon color="red" @click="deleteFile(file.key)">
<Trash/>
</n-icon>
</div>
<div v-if="file.progress == 100">
<a style="color: blue;cursor: pointer" @click="preview(file.key)" target="_blank">点击查看</a>
</div>
</li>
</ul>
<mb-modal ref="videoDialog" title="预览视频" :show-footer="false">
<mb-video :url="currentUrl"/>
</mb-modal>
<mb-modal ref="imageDialog" title="预览图片" :show-footer="false">
<n-image style="margin: 0 auto;display: table" :src="currentUrl"/>
</mb-modal>
</div>
</template>
<script setup>
import ossutil from '@/scripts/ossutil'
import global from '@/scripts/global'
import {Trash, Crop, Add} from "@vicons/ionicons5";
import {ref, onMounted, watch, nextTick} from 'vue'
const emit = defineEmits(['update:modelValue', 'change'])
const props = defineProps({
modelValue: String,
width: String,
height: String,
accept: {
type: String,
default: ''
},
formats: {
type: String,
default: ''
},
readonly: {
type: Boolean,
default: false
}
})
const videoDialog = ref()
const imageDialog = ref()
const currentUrl = ref()
const emitUpdate = ref(true)
watch(() => props.modelValue, (value) => {
if (emitUpdate.value) {
emitUpdate.value = false
if (fileList.value.length == 0) {
fileList.value = renderFileList(value)
}
} else {
fileList.value = renderFileList(value)
}
})
function preview(key) {
key = key.substring(0, key.lastIndexOf('/') + 1) + encodeURIComponent(key.substring(key.lastIndexOf('/') + 1))
let fileUrl = global.filePrefix + key
currentUrl.value = fileUrl
let suffix = key.substring(key.lastIndexOf('\.') + 1)
suffix = suffix.toLocaleLowerCase()
if (suffix == 'mp4') {
videoDialog.value.hide()
nextTick(() => {
videoDialog.value.show()
})
} else if (acceptList.value['image'].indexOf(suffix) != -1) {
imageDialog.value.hide()
nextTick(() => {
imageDialog.value.show()
})
} else {
window.open(fileUrl)
}
}
function renderFileList(urls) {
let fileList = []
if (urls) {
let urlArray = urls.split(',')
for (let i = 0; i < urlArray.length; i++) {
fileList.push({
key: urlArray[i],
name: urlArray[i].substring(urlArray[i].lastIndexOf('/') + 1),
progress: 100
})
}
}
return fileList
}
function updateModelValue() {
let urls = fileList.value.filter(it => it.progress == 100).map(it => it.key).join(',')
emit('update:modelValue', urls)
emitUpdate.value = true
emit('change', urls)
}
const progress = ref()
const fileList = ref(renderFileList(props.modelValue))
const fileInput = ref()
let ossClient = {}
onMounted(async () => {
ossClient = await ossutil.init()
})
function btnClick() {
fileInput.value.click();
}
//
function deleteFile(url) {
fileList.value.forEach((it, i) => {
if (it.key == url) {
fileList.value.splice(i, 1)
}
})
updateModelValue()
ossClient.delete(url);
}
//
const options = {
//
progress: (p, cpt, res) => {
if (p && p == 1) {//
updateModelValue()
}
if (cpt) {
fileList.value.forEach(file => {
if (file.key == cpt.name) {
file.progress = (Math.round(p * 10000) / 100)
if (file.progress == 100) {
updateModelValue()
}
}
})
}
},
//
parallel: 1,
// 1 MB100 KB
partSize: 1024 * 100
}
//
function fileChange() {
const files = fileInput.value.files;
let valid = true
files.forEach(file => {
let fileName = file.name
let accepts = props.accept.split(',')
if (accepts) {
for (let i = 0; i < accepts.length; i++) {
if (!validAccept(fileName, accepts[i]) && valid) {
alert('上传文件格式只能为:' + getSettingSuffixs().replaceAll(',', ''))
valid = false
}
}
} else {
if (!validAccept(fileName, 'null') && valid) {
alert('上传文件格式只能为:' + getAllSuffixs().replaceAll(',', ''))
valid = false
}
}
})
if (!valid) {
return
}
let uploadDirs = []
$common.get('/system/file/getFileUploadDirectory', {length: files.length}).then(dirRes => {
uploadDirs = dirRes.data
let _fileList = []
uploadDirs.forEach((uploadDir, i) => {
let fileUrl = uploadDir + files[i].name;
_fileList.push({key: fileUrl, name: files[i].name, progress: 100, file: files[i]})
})
fileList.value.push(..._fileList)
_fileList.forEach(file => {
ossClient.multipartUpload(file.key, file.file, {
...options,
});
})
fileInput.value.value = ''
})
}
const acceptList = ref({
image: 'png,jpg,gif,jpeg',
wps: 'pdf,pptx,xls,xlsx,csv,docx,doc',
compress: 'zip,rar,7z',
video: 'avi,flv,mp4,mpeg'
})
function getSettingSuffixs() {
if (props.formats) {
return props.formats
}
let suffixs = acceptList.value[props.accept]
if (!suffixs) {
suffixs = getAllSuffixs()
}
return suffixs
}
function getAllSuffixs() {
let suffixs = ''
for (const key in acceptList.value) {
suffixs += acceptList.value[key] + ','
}
suffixs = suffixs.substring(0, suffixs.length - 1)
return suffixs
}
function validAccept(fileName, accept) {
if (props.formats) {
return validEndsWith(fileName, props.formats)
}
if (accept && acceptList.value[accept]) {
return validEndsWith(fileName, acceptList.value[accept])
} else {
return validEndsWith(fileName, getAllSuffixs())
}
}
function validEndsWith(fileName, suffixs) {
suffixs = suffixs.split(',')
for (let i = 0; i < suffixs.length; i++) {
const suffix = suffixs[i]
if (fileName.toLowerCase().endsWith('.' + suffix)) {
return true
}
}
return false
}
</script>

View File

@ -0,0 +1,18 @@
<style scoped>
.video {
margin: 0 auto;
display: table;
}
</style>
<template>
<video width="300" height="300" controls class="video">
<source :src="url" type="video/mp4">
</video>
</template>
<script setup>
const props = defineProps({
url: String
})
</script>

View File

@ -0,0 +1,53 @@
import {computed, watch} from 'vue'
import {isArray, isNumber, isString} from "lodash-es";
export function watchValue(componentValue, props, emit){
let watchList = []
let multiple = props.multiple
let join = props.join
if(!multiple){
join = false
}
const getComponentValue = computed(() => {
if (join) {
return componentValue.value && componentValue.value.join(',')
} else {
return componentValue.value
}
})
let componentValueWatch = false
let setValue = (value) => {
if(isArray(value)){
value = value.map(v => v.toString())
componentValue.value = value
}else if(isNumber(value)){
join = multiple
componentValue.value = props.multiple ? value.toString().split(',') : value.toString()
}else if(isString(value)){
join = multiple
componentValue.value = props.multiple ? value.split(',') : value
}else{
componentValue.value = value
}
if (!componentValueWatch) {
watchList.push(watch(componentValue, (value) => {
if (join) {
emit('update:modelValue', value && value.join(','))
emit('change', value && value.join(','))
} else {
emit('update:modelValue', value)
emit('change', value)
}
}))
}
componentValueWatch = true
}
setValue(props.modelValue)
watchList.push(watch(() => props.modelValue, (value) => {
// 如果传过来的值和选择的值不一样则更新
if(!$common.arrayStringEq(value, getComponentValue.value)){
setValue(value)
}
}))
return watchList
}

View File

@ -0,0 +1,149 @@
<template>
<div :class="monacoVolarClass" style="width: 100%;height: 100%;"></div>
</template>
<script setup>
import { onMounted, ref, watch } from "vue";
import * as monaco from "monaco-editor-core";
import {loadGrammars, loadTheme} from "monaco-volar";
const props = defineProps({
theme: {
type: String,
default: 'dark'// dark or light
},
fileName: {
type: String,
default: ''
},
code: {
type: String,
default: ''
},
oldCode: {
type: String,
default: ''
},
onSave: {
type: Function,
default: () => {}
},
onDidChangeModelContent: {
type: Function,
default: () => {}
},
compare: {
type: Boolean,
default: false
}
})
watch(() => props.code, (value) => {
setValue(value)
})
watch(() => props.oldCode, (value) => {
setOldValue(value)
})
const monacoVolarClass = ref('monaco-volar' + $common.uuid())
function getModelUri(){
let fileName = props.compare ? (props.fileName + 'compare') : props.fileName
return monaco.Uri.parse(`file:///${fileName}.vue`)
}
function getOldModelUri(){
return monaco.Uri.parse(`file:///${props.fileName}-old.vue`)
}
function getModel(){
return monaco.editor.getModel(getModelUri())
}
function getOldModel(){
return monaco.editor.getModel(getOldModelUri())
}
let editorInstance = null
let editorModel = null
let editorOldModel = null
function afterReady(theme) {
editorModel = monaco.editor.createModel(props.code, 'vue', getModelUri());
if(props.compare){
editorInstance = monaco.editor.createDiffEditor(document.querySelector(`.${monacoVolarClass.value}`), {
theme,
automaticLayout: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
inlineSuggest: {
enabled: false,
},
"semanticHighlighting.enabled": true,
})
editorOldModel = monaco.editor.createModel(props.oldCode, 'vue', getOldModelUri());
editorInstance.setModel({
modified: editorModel,
original: editorOldModel
})
}else{
editorInstance = monaco.editor.create(document.querySelector(`.${monacoVolarClass.value}`), {
theme,
model: editorModel,
automaticLayout: true,
scrollBeyondLastLine: false,
minimap: {
enabled: true,
},
inlineSuggest: {
enabled: false,
},
"semanticHighlighting.enabled": true,
})
editorInstance.onDidChangeModelContent((e) => {
props.onDidChangeModelContent(e)
})
addCommands()
loadGrammars(monaco, editorInstance);
}
}
function addCommands(){
editorInstance.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, function(ed) {
props.onSave()
// props.onSave(props.fileName)
// addCommand
});
}
onMounted(() => {
loadTheme(monaco.editor).then(theme => {
afterReady(theme[props.theme]);
})
})
function setValue(value){
getModel().setValue(value)
}
function setOldValue(value){
getOldModel().setValue(value)
}
function getValue(){
return getModel().getValue()
}
function getOldValue(){
return getOldModel().getValue()
}
function dispose(){
editorModel && editorModel.dispose()
editorOldModel && editorOldModel.dispose()
editorInstance.dispose()
}
defineExpose({ setValue, setOldValue, getValue, getOldValue, dispose })
</script>

View File

@ -0,0 +1,9 @@
<template>
<div>{{ msg }}</div>
</template>
<script setup>
import { ref } from 'vue'
const msg = ref('hello magic-boot')
</script>

View File

@ -0,0 +1,145 @@
<template>
<n-form
ref="dataForm"
:rules="rules"
:model="formData"
v-bind="form.props"
>
<n-grid v-for="(row,i) in form.rows" :key="i" :cols="row.gutter">
<n-gi v-for="(col,j) in row.cols" :key="j" :span="col.span" v-bind="col.colProps">
<n-form-item :label="col.label" :label-width="col.labelWidth" :path="col.name"
v-bind="col.formItemProps">
<slot v-if="col.component == 'dynamic'" :name="col.name" :form-data="formData" :col="col"></slot>
<component
v-else
:is="!col.component ? 'mb-input' : col.component.startsWith('n-') || $global.dynamicComponentNames.indexOf(col.component) != -1 ? col.component : 'mb-' + col.component"
v-model="formData[col.name]"
:item-label="col.label"
v-bind="col.props"
@change="col.change"
/>
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</template>
<script setup>
import {ref, reactive, watch} from 'vue'
const rules = reactive(getRules())
const formData = ref({})
const dataForm = ref()
const props = defineProps({
form: {
type: Object,
default: () => {
}
},
detail: {
type: Object,
default: () => {
}
},
add: {
type: Object,
default: () => {
}
},
primaryField: {
type: String,
default: 'id'
}
})
const emit = defineEmits(['reload'])
watch(() => [props.detail && props.detail.formData, props.add && props.add.formData], (value) => {
value.forEach(it => {
if (it) {
formData.value = $common.objectAssign(formData.value, it)
}
})
}, {deep: true})
props.form.props = props.form.props || {}
$common.setDefaultValue(props.form.props, 'labelPosition', 'right')
$common.setDefaultValue(props.form.props, 'labelWidth', '')
if (props.add && props.add.formData) {
formData.value = $common.objectAssign(formData.value, props.add.formData)
}
function getRules() {
let _rules = {}
props.form.rows.forEach(row => {
row.cols.forEach(col => {
if (col.rules) {
_rules[col.name] = col.rules
}
})
})
return _rules
}
function getData() {
let data = {}
props.form.rows.forEach(row => {
row.cols.forEach(col => {
data[col.name] = col.defaultValue || undefined
})
})
return data
}
function initFormData() {
formData.value = getData()
}
function getFormData() {
return formData.value
}
function save(d) {
dataForm.value.validate((errors) => {
if (!errors) {
d.loading()
$common.post(props.form.request.url, formData.value).then(res => {
d.hideLoading()
$message.success((!formData.value[props.primaryField] ? '创建' : '修改') + '成功')
d.hide()
emit('reload')
}).catch(() => d.hideLoading())
}
})
}
function getDetail(id) {
formData.value = props.detail.formData || {}
if (props.detail && props.detail.request) {
let _formData = getData()
_formData[props.primaryField] = id
$common.get(props.detail.request.url, {[props.primaryField]: id}).then(res => {
const {data} = res
for (let t in _formData) {
if ((data[t] || data[t] === 0) && (!props.detail.excludeAssign || props.detail.excludeAssign.indexOf(t) === -1)) {
_formData[t] = data[t]
}
}
if (formData.value) {
formData.value = $common.objectAssign(_formData, formData.value)
} else {
formData.value = _formData
}
if (props.detail.handlerFormData) {
props.detail.handlerFormData(formData.value)
}
})
} else {
if (props.detail.handlerFormData) {
props.detail.handlerFormData(formData.value)
}
}
}
defineExpose({save, getDetail, getFormData, initFormData})
</script>

View File

@ -0,0 +1,63 @@
<template>
<div class="mb-list">
<div class="mb-search">
<mb-search v-if="table.where" :where="table.where" :no-reset="search && search.noReset" @search="reload"/>
</div>
<div class="mb-toolbar" v-if="tools && tools.length > 0">
<template v-for="(it, i) in tools" :key="i">
<n-button v-if="it.type == 'add'" v-permission="it.permission" type="primary" @click="it.click">
{{ it.label || '添加' }}
</n-button>
<!-- <mb-button v-else-if="it.type == 'delete'" v-permission="it.permission" :plain="true" :request-url="it.url" :btn-type="'delete'" :request-data="{ id: ids }" :after-handler="reload" />-->
<n-button v-else :icon="it.icon" :key="it.label" v-permission="it.permission" :type="it.type"
:size="it.size" :class="it.class" @click="it.click(ids)">
{{ it.label }}
</n-button>
</template>
</div>
<div class="mb-table">
<mb-table ref="tableRef" v-bind="table" @selection-change="selectionChange"/>
</div>
</div>
</template>
<script setup>
import {ref} from 'vue'
const tableRef = ref()
const ids = ref([])
const props = defineProps({
search: {
type: Object,
default: () => {
}
},
tools: {
type: Array,
default: () => []
},
table: {
type: Object,
default: () => {
}
}
})
props.tools.forEach(it => {
if (it.type == 'delete') {
props.table.selection = true
}
})
function reload() {
tableRef.value.reload()
}
function selectionChange(columns) {
ids.value = columns.map(it => it['id']).join(',')
}
defineExpose({reload})
</script>

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1646452992174" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4758" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M914.5 653.5c-5.5 0-11 1.1-16 3.3l-0.2 0.1h-0.2L510.2 822.2 122.2 657h-0.2l-0.2-0.1c-5-2.1-10.3-3.3-16-3.3-23.1 0-41.8 19.3-41.8 43.1 0 18 10.7 33.3 25.8 39.8l403.9 172.1 0.4 0.1c10.2 4.4 21.8 4.4 32 0l0.2-0.1c0.1 0 0.1-0.1 0.2-0.1l403.9-172.1c15.1-6.5 25.8-21.8 25.8-39.8 0.1-23.8-18.6-43.1-41.7-43.1z m0-186.5c-7.9-0.2-16 3.2-16 3.2L510.2 635.6 121.8 470.2s-10.3-3.2-16-3.2C82.7 467 64 486.2 64 510c0 17.9 10.7 33.3 25.8 39.7l403.9 172c0.1 0 0.1 0.1 0.2 0.1l0.1 0.1c5 2.1 10.3 3.3 16 3.3 5.7 0 11.1-1.2 16-3.3l0.2-0.1c0.1 0 0.1 0 0.2-0.1l403.9-172c15.1-6.4 25.8-21.8 25.9-39.7 0.1-23.8-18.6-43-41.7-43zM89.8 363.2l403.9 172.1c0.1 0 0.1 0 0.2 0.1l0.1 0.1c5 2.1 10.3 3.2 16 3.2 5.5 0 10.9-1.1 16-3.2l0.2-0.1 0.2-0.1 403.9-172c15.1-6.5 25.8-21.8 25.9-39.7 0-18-10.7-33.3-25.8-39.8L526.5 111.6c-0.1 0-0.1 0-0.2-0.1l-0.2-0.1c-10.2-4.4-21.8-4.4-32 0l-0.1 0.1L89.8 283.7C74.7 290.1 64 305.5 64 323.5c0 17.9 10.7 33.2 25.8 39.7z" p-id="4759"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1653135674579" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5558" xmlns:xlink="http://www.w3.org/1999/xlink" width="240" height="240"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
</style></defs><path d="M170.6752 0h682.6496A170.6752 170.6752 0 0 1 1024 170.6752v682.6496A170.6752 170.6752 0 0 1 853.3248 1024H170.6752A170.6752 170.6752 0 0 1 0 853.3248V170.6752A170.6752 170.6752 0 0 1 170.6752 0z m350.72 682.6752H195.0464a42.6752 42.6752 0 1 0 0 85.3248h326.2976a128 128 0 0 0 241.536 0h72.2432a42.6752 42.6752 0 1 0 0-85.3248H762.88a128 128 0 0 0-241.4336 0zM263.2704 256H192.5376a42.6752 42.6752 0 1 0 0 85.3248h70.7584a128 128 0 0 0 241.4336 0h327.68a42.6752 42.6752 0 0 0 0-85.3248h-327.68a128 128 0 0 0-241.4336 0z" p-id="5559"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1649004377679" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2837" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
</style></defs><path d="M511.850044 0.299912C229.332813 0.299912 0.299912 107.568486 0 239.929708v544.140584c0 132.461193 229.132871 239.929708 511.850044 239.929708s511.850044-107.468515 511.850044-239.929708V239.929708C1023.400176 107.568486 794.367275 0.299912 511.850044 0.299912zM167.950796 895.737577c-22.093527 0-39.988285-17.894757-39.988285-39.988285s17.894757-39.988285 39.988285-39.988284 39.988285 17.894757 39.988284 39.988284-17.894757 39.988285-39.988284 39.988285z m791.768036-188.644733c-17.894757 11.496632-37.489017 22.193498-58.782778 32.190569-104.969247 49.18559-243.228742 76.277653-389.08601 76.277653s-284.116763-27.092063-389.08601-76.277653c-21.293762-9.997071-40.888021-20.693937-58.782779-32.190569v-79.176804c87.274431 73.778385 255.125256 123.66377 447.868789 123.663771s360.594357-49.885385 447.868788-123.663771v79.176804zM127.962511 583.828956c0-22.093527 17.894757-39.988285 39.988285-39.988284s39.988285 17.894757 39.988284 39.988284-17.894757 39.988285-39.988284 39.988285-39.988285-17.894757-39.988285-39.988285z m831.756321-148.156594c-17.894757 11.496632-37.489017 22.193498-58.782778 32.190569-104.969247 49.18559-243.228742 76.277653-389.08601 76.277653S227.733281 517.048521 122.764034 467.862931c-21.293762-9.997071-40.888021-20.693937-58.782779-32.190569v-79.176804c87.274431 73.778385 255.125256 123.66377 447.868789 123.66377s360.594357-49.885385 447.868788-123.66377v79.176804z" p-id="2838"></path></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1641017183540" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5317" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M259.2 0H896a64 64 0 0 1 64 64v748.608a64 64 0 0 1-64 64H259.2a64 64 0 0 1-64-64V64a64 64 0 0 1 64-64z m132.8 183.04V640h171.264c76.096 0 133.12-20.48 172.544-61.44 37.376-39.04 56.384-94.72 56.384-167.04 0-72.96-19.008-128.64-56.384-167.04-39.36-40.96-96.448-61.44-172.544-61.44H392z m76.8 64h80c58.432 0 101.056 12.8 128 39.04 26.24 25.6 39.296 67.84 39.296 125.44 0 56.32-13.12 97.92-39.36 124.8-26.88 26.24-69.504 39.68-127.936 39.68h-80V247.04zM64 384h75.968v568.512h580.096V1024H128a64 64 0 0 1-64-64V384z" p-id="5318"></path></svg>

After

Width:  |  Height:  |  Size: 915 B

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1028 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M875.086452 153.730058C676.053818-45.302575 353.260522-45.302575 154.128323 153.730058s-199.032634 521.825929 0 720.958129 521.825929 199.032634 720.958129 0 199.032634-521.825929 0-720.958129zM725.836868 725.438604c-9.757478 9.757478-25.488922 9.757478-35.246399 0L514.557604 549.405739 338.624306 725.438604c-9.757478 9.757478-25.488922 9.757478-35.2464 0s-9.757478-25.488922 0-35.2464l176.032865-176.032864-176.032865-175.933299c-9.757478-9.757478-9.757478-25.488922 0-35.246399 9.757478-9.757478 25.488922-9.757478 35.2464 0l176.032864 176.032865 176.032865-176.032865c9.757478-9.757478 25.488922-9.757478 35.246399 0 9.757478 9.757478 9.757478 25.488922 0 35.246399L549.804004 514.15934 725.836868 690.192204c9.657912 9.757478 9.657912 25.488922 0 35.2464z"></path></svg>

After

Width:  |  Height:  |  Size: 899 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1641017365938" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6713" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M922.317764 0H101.64469A100.312307 100.312307 0 0 0 1.634036 100.010654V923.989346a100.312307 100.312307 0 0 0 100.010654 100.010654h820.673074a100.299739 100.299739 0 0 0 99.998085-100.010654V100.06093A100.299739 100.299739 0 0 0 922.317764 0zM427.933063 402.04107l-177.321894 103.064894 177.321894 97.094672v81.257871L179.684925 534.831388v-57.477532l248.248138-160.529857z m70.938812 364.661192h-53.970812l78.643542-509.303973h53.958243z m345.393085-231.845736L596.004253 683.445938v-81.257871l177.321894-97.094672-177.321894-103.064894v-85.217071l248.260707 160.542426z" p-id="6714"></path></svg>

After

Width:  |  Height:  |  Size: 977 B

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>

After

Width:  |  Height:  |  Size: 944 B

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M38.47 52L52 38.462l-23.648-23.67L43.209 0H.035L0 43.137l14.757-14.865L38.47 52zm74.773 47.726L89.526 76 76 89.536l23.648 23.672L84.795 128h43.174L128 84.863l-14.757 14.863zM89.538 52l23.668-23.648L128 43.207V.038L84.866 0 99.73 14.76 76 38.472 89.538 52zM38.46 76L14.792 99.651 0 84.794v43.173l43.137.033-14.865-14.757L52 89.53 38.46 76z"/></svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1641017648725" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19289" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M555.541333 117.994667l312.874667 224.565333A117.333333 117.333333 0 0 1 917.333333 437.866667V800c0 64.8-52.533333 117.333333-117.333333 117.333333H640V746.666667c0-70.688-57.312-128-128-128s-128 57.312-128 128v170.666666H224c-64.8 0-117.333333-52.533333-117.333333-117.333333V437.877333a117.333333 117.333333 0 0 1 48.917333-95.317333l312.874667-224.565333a74.666667 74.666667 0 0 1 87.082666 0z" p-id="19290"></path></svg>

After

Width:  |  Height:  |  Size: 803 B

View File

@ -0,0 +1,2 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1649004239313" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1045" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
</style></defs><path d="M870.4 256h-153.6V153.6a51.2 51.2 0 0 0-102.4 0v102.4H409.6V153.6a51.2 51.2 0 0 0-102.4 0v102.4H153.6a51.2 51.2 0 0 0 0 102.4h51.2v102.4a51.2 51.2 0 0 0 0 12.8A322.56 322.56 0 0 0 204.8 512a307.2 307.2 0 0 0 256 302.592V921.6h102.4v-107.008A307.2 307.2 0 0 0 819.2 512a322.56 322.56 0 0 0 0-38.4A51.2 51.2 0 0 0 819.2 460.8V358.4h51.2a51.2 51.2 0 0 0 0-102.4z m-153.6 204.8h-7.168a209.408 209.408 0 0 1 7.168 51.2 204.8 204.8 0 0 1-409.6 0 209.408 209.408 0 0 1 7.168-51.2H307.2V358.4h409.6z" p-id="1046"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1646472156835" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5189" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M956.8 828.7c3.2 20-0.8 38-12 54-11.1 16-26.4 25.7-45.5 29l-115.1 19c-18.5 3.3-35.7-0.9-51.3-12.5-15.6-11.6-25.1-27.5-28.3-47.5l-97.8-646c-1.3-10-0.8-19.5 1.4-28.5s6-17.4 11-25c5.1-7.7 11.7-14 19.7-19s16.8-8.5 26.4-10.5l114.1-19c19.2-3.3 36.4 1 51.8 13 15.4 12 24.6 28 27.8 48l97.8 645zM510 99.6c19.2 0 35.5 7 48.9 21 13.4 14 20.1 31 20.1 51v667.9c0 20-6.7 37-20.1 51-13.4 14-29.7 21-48.9 21h-99.7c-19.2 0-35.5-7-48.9-21-13.4-14-20.1-31-20.1-51V171.6c0-20 5.1-37 15.4-51s25.3-21 45-21H510z m23.4 504c6.4 0 11.8-3.1 16.3-9.5 4.5-6.3 6.7-13.9 6.7-22.5 0-9.4-2.3-17-6.7-23-4.5-6-9.9-9-16.3-9h-99.7c-6.4 0-11.8 3-16.3 9s-6.7 13.7-6.7 23c0 8.7 2.2 16.2 6.7 22.5 4.5 6.4 9.9 9.5 16.3 9.5h99.7z m0-127.1c6.4 0 11.8-3.1 16.3-9.5 4.5-6.3 6.7-14.2 6.7-23.5s-2.3-17-6.7-23c-4.5-6.1-9.9-9-16.3-9h-99.7c-6.4 0-11.8 3-16.3 9s-6.7 13.7-6.7 23 2.2 17.2 6.7 23.5c4.5 6.3 9.9 9.5 16.3 9.5h99.7z m-301-376.9c19.2 0 35.7 7 49.3 21 13.7 14 20.6 31 20.6 51v667.9c0 20-6.8 37-20.6 51s-30.2 21-49.3 21h-99.7c-19.2 0-35.7-7-49.3-21-13.8-14-20.6-31-20.6-51V171.6c0-20 6.8-37 20.6-51s30.2-21 49.3-21h99.7z m-95.9 250c-7 0-12.8 3-17.2 9-4.5 6-6.7 13.7-6.7 23 0 8.7 2.2 16.2 6.7 22.5 4.5 6.2 10.2 9.5 17.2 9.5h92c7 0 12.6-3.1 16.8-9.5s6.3-13.9 6.3-22.5c0-9.4-2.1-17-6.3-23-4.1-6-9.7-9-16.8-9h-92z m95.9 313.9c7 0 12.6-3 16.8-9 4.1-6 6.3-13.4 6.3-22 0-9.4-2.1-16.8-6.3-22.5s-9.7-8.5-16.8-8.5h-95.9c-7 0-12.8 2.8-17.2 8.5-4.4 5.7-6.7 13.2-6.7 22.5 0 8.7 2.2 16 6.7 22s10.2 9 17.2 9h95.9z m0-122.9c7 0 12.6-3 16.8-9 4.1-6 6.3-13.7 6.3-23 0-9.4-2.1-17-6.3-23-4.1-6-9.7-9-16.8-9h-95.9c-7 0-12.8 3-17.2 9-4.5 6-6.7 13.7-6.7 23 0 9.4 2.2 17 6.7 23 4.5 5.9 10.2 9 17.2 9h95.9z" p-id="5190"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1649004325581" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2700" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
</style></defs><path d="M665.729808 153.544368V0h-102.733792v153.544368h-255.90728V0H204.725824v153.544368H0v153.544368h870.455632V153.544368h-204.725824zM0 358.270192h255.90728v255.90728H0zM562.996016 430.220934a205.096704 205.096704 0 0 0-51.181456 132.775082 170.604853 170.604853 0 0 0 5.192322 51.181456H307.088736v-255.90728h255.90728zM0 665.358928h255.90728v255.90728H0zM562.996016 696.141978a363.833394 363.833394 0 0 0-189.148859 225.49511h-66.758421v-256.27816h230.316552z" p-id="2701"></path><path d="M562.996016 562.996016A153.915248 153.915248 0 1 0 716.911264 407.968127a153.915248 153.915248 0 0 0-153.915248 153.544368z" p-id="2702"></path><path d="M716.911264 716.911264A308.201376 308.201376 0 0 0 407.968127 1024h616.031873a307.830496 307.830496 0 0 0-307.088736-307.088736z" p-id="2703"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1641017024921" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2935" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M892.928 128q28.672 0 48.64 19.968t19.968 48.64l0 52.224q0 28.672-19.968 48.64t-48.64 19.968l-759.808 0q-28.672 0-48.64-19.968t-19.968-48.64l0-52.224q0-28.672 19.968-48.64t48.64-19.968l759.808 0zM892.928 448.512q28.672 0 48.64 19.968t19.968 48.64l0 52.224q0 28.672-19.968 48.64t-48.64 19.968l-759.808 0q-28.672 0-48.64-19.968t-19.968-48.64l0-52.224q0-28.672 19.968-48.64t48.64-19.968l759.808 0zM892.928 769.024q28.672 0 48.64 19.968t19.968 48.64l0 52.224q0 28.672-19.968 48.64t-48.64 19.968l-759.808 0q-28.672 0-48.64-19.968t-19.968-48.64l0-52.224q0-28.672 19.968-48.64t48.64-19.968l759.808 0z" p-id="2936"></path></svg>

After

Width:  |  Height:  |  Size: 997 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1646494062534" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2291" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M759.3 525.4h-200v199.4c0 110.2 89.6 199.4 200 199.4 110.5 0 199.9-89.3 199.9-199.4 0.1-110.2-89.4-199.4-199.9-199.4zM98 724.8c0 110.2 89.6 199.4 199.9 199.4 110.5 0 199.9-89.2 199.9-199.4V525.4H297.9C187.6 525.4 98 614.6 98 724.8z m861.3-460.3c0-110.2-89.6-199.4-199.9-199.4-110.4 0-200 89.3-200 199.4v199.4h200c110.4 0 199.9-89.2 199.9-199.4zM297.9 65.1C187.5 65.1 98 154.5 98 264.5c0 110.2 89.6 199.4 199.9 199.4h200V264.5c0-110.1-89.5-199.4-200-199.4z" p-id="2292"></path></svg>

After

Width:  |  Height:  |  Size: 860 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1641720023979" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15481" width="240" height="240" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M960 1024h-320v-384h128V576H256v64h128v384H0v-384h128V448h320V384H320V0h384v384H576v64h320v192h128v384h-64zM128 896h128v-128H128v128zM576 128H448v128h128V128z m320 640h-128v128h128v-128z" p-id="15482"></path></svg>

After

Width:  |  Height:  |  Size: 592 B

View File

@ -0,0 +1,2 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1649004383477" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2972" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
</style></defs><path d="M12.795383 550.784512h92.979783a11.942357 11.942357 0 0 1 12.795383 11.942357v12.795383a12.795383 12.795383 0 0 1-12.795383 11.942358H12.795383a12.36887 12.36887 0 0 1-12.795383-11.942358v-12.795383a11.942357 11.942357 0 0 1 12.795383-11.942357z m0-365.521441h92.979783a11.942357 11.942357 0 0 1 12.795383 11.942357v12.795383a11.942357 11.942357 0 0 1-12.795383 11.942358H12.795383a11.942357 11.942357 0 0 1-12.795383-11.942358v-12.795383a11.942357 11.942357 0 0 1 12.795383-11.942357z m0 182.973977h92.979783a11.942357 11.942357 0 0 1 12.795383 11.515844v13.221896a11.942357 11.942357 0 0 1-12.795383 11.515845H12.795383a11.942357 11.942357 0 0 1-12.795383-11.515845V379.752892a11.942357 11.942357 0 0 1 12.795383-11.515844z m0 365.094928h92.979783a12.36887 12.36887 0 0 1 12.795383 11.942358v12.795383a11.942357 11.942357 0 0 1-12.795383 11.942357H12.795383a11.942357 11.942357 0 0 1-12.795383-11.942357v-12.795383a12.36887 12.36887 0 0 1 12.795383-11.942358z" p-id="2973"></path><path d="M945.578804 73.090213h-69.521581a10.236306 10.236306 0 0 1-10.236307-8.103742A76.772298 76.772298 0 0 0 787.76908 0.15653H127.95383A85.302553 85.302553 0 0 0 37.533123 82.473494v54.593634a9.383281 9.383281 0 0 0 9.809794 9.383281H106.628192a47.76943 47.76943 0 0 1 49.475481 45.210353v27.296817a38.386149 38.386149 0 0 1-39.665688 37.106611H47.342917a9.383281 9.383281 0 0 0-9.809794 8.956768v54.593634a9.383281 9.383281 0 0 0 9.809794 9.383281H106.628192a47.76943 47.76943 0 0 1 49.475481 46.063379v17.913536a48.195943 48.195943 0 0 1-49.475481 46.063379H47.342917a9.383281 9.383281 0 0 0-9.809794 8.956768v54.593634a9.383281 9.383281 0 0 0 9.809794 9.383281H106.628192a47.76943 47.76943 0 0 1 49.048968 45.636866v27.296817a38.386149 38.386149 0 0 1-39.665687 36.680098H47.342917a9.809794 9.809794 0 0 0-10.236306 9.383281v54.593634a9.809794 9.809794 0 0 0 10.236306 9.383281h58.858762a47.342917 47.342917 0 0 1 49.048968 45.636866V767.87951a37.959636 37.959636 0 0 1-39.239174 36.253585H46.916404a10.236306 10.236306 0 0 0-10.236306 9.383281v127.95383A85.302553 85.302553 0 0 0 127.95383 1023.78717h810.374257a85.302553 85.302553 0 0 0 85.302553-82.316964V146.450409a76.345785 76.345785 0 0 0-78.904862-73.360196z m-343.342777 91.273732a47.76943 47.76943 0 0 1 55.446659-13.648408l94.259322 49.048968A37.959636 37.959636 0 0 1 767.72298 249.239986l-298.558937 466.178454a10.662819 10.662819 0 0 1-13.221895 3.412102l-147.573418-76.345785a8.956768 8.956768 0 0 1-3.838615-12.36887z m-178.70885 597.117874l-119.850087 42.651276a10.662819 10.662819 0 0 1-12.795383-5.544666v-2.559076l-9.809794-107.90773a9.383281 9.383281 0 0 1 8.956768-9.809794 13.648409 13.648409 0 0 1 5.971179 0l127.95383 66.962505a8.956768 8.956768 0 0 1 3.838615 12.36887 8.530255 8.530255 0 0 1-4.69164 4.69164z m522.051627 152.265057c0 40.0922-2.559077 72.933683-78.904862 72.933683H156.103673a76.772298 76.772298 0 0 1-78.051837-62.697376 8.956768 8.956768 0 0 1 7.250717-10.236307h692.230221a85.302553 85.302553 0 0 0 88.714655-82.316964V118.727079a9.809794 9.809794 0 0 1 10.236306-8.956768c38.812662 0 68.668555 5.118153 68.668556 72.933683v731.042882z" p-id="2974"></path></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M108.8 44.322H89.6v-5.36c0-9.04-3.308-24.163-25.6-24.163-23.145 0-25.6 16.881-25.6 24.162v5.361H19.2v-5.36C19.2 15.281 36.798 0 64 0c27.202 0 44.8 15.281 44.8 38.961v5.361zm-32 39.356c0-5.44-5.763-9.832-12.8-9.832-7.037 0-12.8 4.392-12.8 9.832 0 3.682 2.567 6.808 6.407 8.477v11.205c0 2.718 2.875 4.962 6.4 4.962 3.524 0 6.4-2.244 6.4-4.962V92.155c3.833-1.669 6.393-4.795 6.393-8.477zM128 64v49.201c0 8.158-8.645 14.799-19.2 14.799H19.2C8.651 128 0 121.359 0 113.201V64c0-8.153 8.645-14.799 19.2-14.799h89.6c10.555 0 19.2 6.646 19.2 14.799z"/></svg>

After

Width:  |  Height:  |  Size: 623 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1641017706517" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="20110" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M792.703177 534.452774c-49.288352 33.217439-108.257215 52.515189-171.718341 52.515189-63.271312 0-122.050361-19.171208-171.1489-52.135561-132.996298 65.042909-224.866244 203.290726-224.866244 363.303875 0 169.314031 793.232441 166.972993 793.232441 0C1018.138862 737.7435 926.079103 599.305869 792.703177 534.452774L792.703177 534.452774 792.703177 534.452774zM354.359526 270.99103c0-149.636653 119.329695-270.99103 266.62531-270.99103 147.295615 0 266.62531 121.291105 266.62531 270.99103 0 149.636653-119.329695 270.864488-266.62531 270.864488C473.68922 541.855518 354.359526 420.627684 354.359526 270.99103L354.359526 270.99103 354.359526 270.99103 354.359526 270.99103zM177.959107 912.182508c0 6.200589 0.94907 12.084821 2.783938 17.905781C80.584558 913.827562 5.797867 881.749007 5.797867 833.409724c0-138.500902 79.468768-258.146954 194.622556-314.458422 40.177283 27.01685 88.010395 43.340849 139.576515 45.049174C241.040605 646.696082 177.959107 771.97328 177.959107 912.182508L177.959107 912.182508 177.959107 912.182508zM310.006336 270.99103c0-68.775916 21.638789-132.426856 58.399421-184.309332C361.952083 86.112256 355.561681 85.859171 349.108007 85.859171c-121.227834 0-219.488182 99.842131-219.488182 223.031376s98.197077 223.031376 219.424911 223.031376c27.269936 0 53.400988-5.061705 77.444086-14.362588C355.498409 459.666083 310.006336 370.76989 310.006336 270.99103L310.006336 270.99103zM310.006336 270.99103" p-id="20111"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1703302857634" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2481" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.1953125" height="200"><path d="M651.8 775.5h-20.3v-527h178V305c0 19.3 15.7 35 35 35s35-15.7 35-35v-91.5c0-19.3-15.7-35-35-35h-248c-19.3 0-35 15.7-35 35V477H340.8V236.1c0-32-26-58-58-58H58c-32 0-58 26-58 58v551.8c0 32 26 58 58 58h224.8c32 0 58-26 58-58V547h220.7v263.5c0 19.3 15.7 35 35 35h55.3c19.3 0 35-15.7 35-35s-15.7-35-35-35z m-381 0.4H70V248.1h200.8v527.8z" p-id="2482"></path><path d="M990 651.5h-89v-89c0-19.3-15.7-35-35-35s-35 15.7-35 35v89h-89c-19.3 0-35 15.7-35 35s15.7 35 35 35h89v89c0 19.3 15.7 35 35 35s35-15.7 35-35v-89h89c19.3 0 35-15.7 35-35s-15.7-35-35-35z" p-id="2483"></path></svg>

After

Width:  |  Height:  |  Size: 911 B

View File

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M124.884 109.812L94.256 79.166c-.357-.357-.757-.629-1.129-.914a50.366 50.366 0 0 0 8.186-27.59C101.327 22.689 78.656 0 50.67 0 22.685 0 0 22.688 0 50.663c0 27.989 22.685 50.663 50.656 50.663 10.186 0 19.643-3.03 27.6-8.201.286.385.557.771.9 1.114l30.628 30.632a10.633 10.633 0 0 0 7.543 3.129c2.728 0 5.457-1.043 7.543-3.115 4.171-4.157 4.171-10.915.014-15.073M50.671 85.338C31.557 85.338 16 69.78 16 50.663c0-19.102 15.557-34.661 34.67-34.661 19.115 0 34.657 15.559 34.657 34.675 0 19.102-15.557 34.661-34.656 34.661"/></svg>

After

Width:  |  Height:  |  Size: 600 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1641015979047" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2139" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M519.9 647.4c-44.8 7.4-90.4-6.9-122.5-38.3-32.1-31.4-46.6-76.2-39-120 9.4-56.8 56.2-102.7 114.2-112.1 44.9-7.6 90.7 6.7 122.9 38.2 32.2 31.6 46.7 76.5 38.9 120.4-9.7 56.9-56.5 102.6-114.5 111.8M848.4 559l66-40.4c10.5-6.4 15.6-18.6 12.9-30.4L895 349.3c-2.7-11.8-12.8-20.6-25.1-21.9l-77.5-8.4c-7.7-0.8-14.7-4.6-19.5-10.5l-13-16c-4.8-5.9-7-13.5-6.1-21l8.9-75.7c1.4-12-5.1-23.6-16.2-28.9l-131-61.8c-11.1-5.2-24.4-3-33.2 5.5l-55 54c-5.4 5.3-12.8 8.4-20.6 8.4H486c-7.7 0-15.1-3-20.6-8.4l-55-54c-8.7-8.6-22-10.8-33.2-5.5l-130.9 61.8c-11.1 5.3-17.6 16.8-16.2 28.9l8.9 75.8c0.9 7.5-1.3 15-6.1 21L220 308.4c-4.8 5.9-11.8 9.7-19.5 10.5l-77.5 8.5c-12.3 1.3-22.3 10.1-25.1 21.9L65.5 488.1c-2.7 11.8 2.4 24 12.9 30.4l66 40.5c6.5 4 11.2 10.4 12.9 17.8l4.6 19.9c1.7 7.4 0.4 15.1-3.7 21.5l-41.6 64.5c-6.6 10.2-5.8 23.4 1.9 32.9l90.7 111.3c7.7 9.5 20.7 13.1 32.3 9.1l73.5-25.3c7.3-2.5 15.3-2.1 22.2 1.2l18.7 8.8c7 3.3 12.3 9.1 14.8 16.3l25.6 72c4.1 11.4 15.1 19.1 27.4 19.1H569c12.4 0 23.4-7.7 27.4-19.1l25.6-72c2.5-7.1 7.9-13 14.8-16.3l18.7-8.8c6.9-3.3 14.9-3.7 22.2-1.2l73.5 25.3c11.7 4 24.6 0.4 32.3-9.1l90.7-111.3c7.7-9.5 8.4-22.7 1.9-32.9l-41.6-64.5c-4.1-6.4-5.5-14.1-3.8-21.5l4.6-19.8c1.9-7.5 6.5-13.9 13.1-17.9" fill="" p-id="2140"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1703302854435" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2343" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M928 701c-8.8 0-16-7.2-16-16v-77c0-53-43-96-96-96H560c-8.8 0-16-7.2-16-16v-96c0-8.8 7.2-16 16-16h48c53 0 96-43 96-96V96c0-53-43-96-96-96H416c-53 0-96 43-96 96v192c0 53 43 96 96 96h48c8.8 0 16 7.2 16 16v96c0 8.8-7.2 16-16 16H208c-53 0-96 43-96 96v77c0 8.8-7.2 16-16 16-53 0-96 43-96 96v131c0 53 43 96 96 96h96c53 0 96-43 96-96V797c0-53-43-96-96-96-8.8 0-16-7.2-16-16v-77c0-17.7 14.3-32 32-32h256c8.8 0 16 7.2 16 16v94c0 8.3-6.7 15-15 15h-1c-53 0-96 43-96 96v131c0 53 43 96 96 96h96c53 0 96-43 96-96V797c0-53-43-96-96-96h-1c-8.3 0-15-6.7-15-15v-94c0-8.8 7.2-16 16-16h256c17.7 0 32 14.3 32 32v77c0 8.8-7.2 16-16 16-53 0-96 43-96 96v131c0 53 43 96 96 96h96c53 0 96-43 96-96V797c0-53-43-96-96-96z m-736 64c17.6 0 32 14.4 32 32v131c0 17.6-14.4 32-32 32H96c-17.6 0-32-14.4-32-32V797c0-17.6 14.4-32 32-32h96z m368 0c17.6 0 32 14.4 32 32v131c0 17.6-14.4 32-32 32h-96c-17.6 0-32-14.4-32-32V797c0-17.6 14.4-32 32-32h96zM416 320c-17.6 0-32-14.4-32-32V96c0-17.6 14.4-32 32-32h192c17.6 0 32 14.4 32 32v192c0 17.6-14.4 32-32 32H416z m544 608c0 17.6-14.4 32-32 32h-96c-17.6 0-32-14.4-32-32V797c0-17.6 14.4-32 32-32h96c17.6 0 32 14.4 32 32v131z" p-id="2344"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z"/></svg>

After

Width:  |  Height:  |  Size: 424 B

View File

@ -0,0 +1,2 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1649004142311" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3942" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
</style></defs><path d="M522.854 33.884L902.47 164.732c14.1 4.85 23.54 18.113 23.54 33.011v402.218c-10.714 238.49-386.18 383.703-402.118 389.764A34.91 34.91 0 0 1 511.505 992c-4.217 0-8.439-0.767-12.387-2.275C483.147 983.664 107.648 838.452 97 601.502V197.743c0-14.898 9.442-28.16 23.503-33.011L500.121 33.884a35.24 35.24 0 0 1 22.733 0z m243.984 299.804c-29.29-29.29-76.777-29.29-106.066 0L459.246 535.213l-95.46-95.46c-29.289-29.289-76.776-29.289-106.065 0-29.29 29.29-29.29 76.777 0 106.067l148.492 148.492c14.645 14.645 33.839 21.967 53.033 21.967l1.152-0.009c18.808-0.287 37.53-7.606 51.881-21.958l254.559-254.558c29.289-29.29 29.289-76.777 0-106.066z" p-id="3943"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<template>
<n-result status="404" title="404 资源不存在" description="生活总归带点荒谬" />
</template>

View File

@ -0,0 +1,176 @@
<template>
<n-layout position="absolute" has-sider>
<n-layout-sider
bordered
collapse-mode="width"
:collapsed-width="64"
:width="200"
show-trigger
@update:collapsed="updateCollapsed"
@after-enter="() => isCollapsed = false"
>
<n-layout position="absolute">
<n-layout-header class="nav-bg">
<p class="text-center text-2xl m-0 pt-5 pb-5 text-white title" v-if="!isCollapsed">
{{ $global.title }}
</p>
<p class="text-center text-2xl m-0 pt-5 pb-5 text-white title" v-else>
{{ $global.title.substring(0, 1) }}
</p>
</n-layout-header>
<n-layout-content class="absolute top-16 right-0 bottom-0 left-0 nav-bg" :native-scrollbar="false">
<n-menu
ref="menuRef"
v-model:value="selectedKey"
:indent="24"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="menuOptions"
inverted
accordion
/>
</n-layout-content>
</n-layout>
</n-layout-sider>
<n-layout>
<n-layout-header class="h-16" style="box-shadow: 1px 1px 6px #c6c6c6">
<layout-header/>
</n-layout-header>
<n-layout-content class="absolute right-0 bottom-0 left-0 bg-lightgray" style="top:4.3rem;">
<n-layout position="absolute">
<n-layout-header class="h-12 p-2 bg-lightgray">
<tabs/>
</n-layout-header>
<n-layout-content class="absolute top-12 right-0 bottom-0 left-0 px-4 router-view-content p-1 bg-lightgray">
<div style="width: 100%;height: 100%">
<component
v-for="com in keepaliveIframes"
:key="com.path"
:is="IframeComponent"
:url="common.getUrlType(com.meta.path) == 2 ? '/#' + com.meta.path : com.meta.path"
v-show="com.path == $route.path"
/>
<component
v-for="com in keepaliveDynamicComponents"
:key="com.path"
:is="ShowComponent"
:name="com.meta.componentName"
v-show="com.path == $route.path"
/>
<nested-router />
</div>
</n-layout-content>
</n-layout>
</n-layout-content>
</n-layout>
</n-layout>
</template>
<script setup>
import {ref, h, watch, computed} from 'vue';
import tabs from './tabs.vue'
import NestedRouter from './nested-router.vue'
import {RouterLink} from 'vue-router'
// template 使$common 使
import common from '@/scripts/common'
import {useUserStore} from "@/store/modules/userStore"
import {useTabsStore} from "@/store/modules/tabsStore"
import LayoutHeader from "@/layout/layout-header.vue";
import IframeComponent from '@/views/common/iframe.vue'
import ShowComponent from '@/views/common/show-component.vue'
import { isEmpty } from 'lodash-es'
const tabsStore = useTabsStore()
const userStore = useUserStore()
const menuRef = ref()
const currentTab = tabsStore.getCurrentTab
const selectedKey = ref(currentTab)
selectMenu(currentTab)
watch(() => tabsStore.getCurrentTab, (key) => selectMenu(key))
// "iframe"
const keepaliveIframes = computed(() => tabsStore.getTabs.filter(it => $common.filterIframeTabs(it)))
//
const keepaliveDynamicComponents = computed(() => tabsStore.getTabs.filter(it => it.meta.componentName && it.meta.keepAlive))
function selectMenu(key) {
selectedKey.value = key
menuRef.value?.showOption(key);
}
const isCollapsed = ref(false)
function updateCollapsed(collapsed) {
if (collapsed) {
isCollapsed.value = collapsed
}
}
const menuOptions = ref(recursionRouters(userStore.getPermissionRouters))
function recursionRouters(children) {
let menus = []
children.forEach((chi) => {
let menu = {}
if (!isEmpty(chi.children)) {
if (chi['alwaysShow'] === true) {
menu.key = chi.path
menu.label = chi.title
menu.children = recursionRouters(chi.children)
} else {
menu.key = chi.children[0].path
menu.label = () => h(
RouterLink,
{
to: {
path: chi.children[0].path
}
},
{default: () => chi.children[0].title}
)
}
} else {
menu.key = chi.path
if(chi.openMode == '1'){
let path = $common.handlerUrlPage(chi.path)
menu.label = () => h(
'a',
{
href: path,
target: '_blank'
},
chi.title
)
}else{
menu.label = () => h(
RouterLink,
{
to: {
path: chi.path
}
},
{default: () => chi.title}
)
}
}
if (chi.icon) {
menu.icon = $common.renderIcon(chi.icon)
}
menus.push(menu)
})
return menus
}
</script>
<style scoped>
.nav-bg {
background-color: #041427;
}
.title {
font-family: PoetsenOne;
}
.bg-lightgray{
background-color: #fafafa;
}
</style>

View File

@ -0,0 +1,126 @@
<template>
<div class="p-4">
<div class="header">
<div style="width: 95%">
<div>
<n-breadcrumb>
<n-breadcrumb-item>
首页
</n-breadcrumb-item>
<n-breadcrumb-item>
{{ $router.currentRoute.value.meta.title }}
</n-breadcrumb-item>
</n-breadcrumb>
</div>
</div>
<div>
<template v-if="userStore.getInfo.headPortrait">
<n-avatar
round
:size="40"
:src="$global.baseApi + userStore.getInfo.headPortrait"
/>
</template>
<n-avatar
v-else
round
:size="40"
>
{{ userStore.getInfo.name?.substring(0, 1) }}
</n-avatar>
</div>
<div>
<n-dropdown :options="options" @select="handleSelect">
<n-button text style="font-size: 20px;text-align: center">
<mb-icon icon="SettingsOutline" />
</n-button>
</n-dropdown>
</div>
</div>
<mb-modal ref="uiDialog" title="UI大小配置" @confirm="handleUiSetting" width="320px" :show-footer="false">
<div class="flex justify-center">
<n-radio-group v-model:value="$global.uiSize.value" name="uiSize" size="large">
<n-radio-button
v-for="ui in uiSizeList"
:key="ui.value"
:value="ui.value"
>
{{ ui.label }}
</n-radio-button>
</n-radio-group>
</div>
</mb-modal>
</div>
</template>
<script setup>
import {ref} from 'vue'
import {useUserStore} from "@/store/modules/userStore";
import router from '@/scripts/router'
const userStore = useUserStore()
const uiSizeList = ref([
{
label: "紧凑(小)",
value: "small"
},
{
label: "正常(中)",
value: "medium"
},
{
label: "放大(大)",
value: "large"
}
])
const options = ref([
{
label: "个人中心",
key: "userCenter",
},
{
label: "界面大小",
key: "uiSetting",
},
{
label: "退出",
key: "logout",
},
])
const uiDialog = ref();
function handleSelect(key) {
switch (key) {
case 'logout':
userStore.logout()
break;
case 'userCenter':
router.push({
path: '/user-center'
})
break;
case 'uiSetting':
uiDialog.value.show();
break;
}
}
function handleUiSetting() {
console.log();
}
</script>
<style scoped lang="less">
.header {
display: flex;
justify-content: space-around;
align-items: center;
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in" appear>
<keep-alive :include="keepAliveInclude">
<component v-if="tabsStore.getShow && routerComponents.some(it => it.path === $route.path)" :is="Component" :key="$route.path"/>
</keep-alive>
</transition>
</router-view>
</template>
<script setup>
import {useTabsStore} from '@/store/modules/tabsStore'
import {computed} from "vue";
const tabsStore = useTabsStore()
const routerComponents = computed(() => tabsStore.getTabs.filter(it => {
//
if(it.meta.keepAlive && it.meta.componentName){
return false
}
// iframe
return !$common.filterIframeTabs(it);
}))
const keepAliveInclude = computed(() => tabsStore.getTabs.filter(it => it.meta.keepAlive && !it.meta.componentName).map(it => it.path.substring(it.path.lastIndexOf('/') + 1)))
</script>

View File

@ -0,0 +1,183 @@
<template>
<div class="tabs">
<n-tag
:size="$global.uiSize.value"
@contextmenu="handleContextMenu(tab,$event)"
v-for="tab in tabsStore.getTabs"
:closable="tab.path!==`/home`"
@close="handleClose(tab.path)"
@click="jump(tab)"
:type="tabsStore.getCurrentTab == tab.path ? 'primary' : 'default'"
:class="[tabsStore.getCurrentTab == tab.path?'selected':'',global.uiSize]"
:bordered="false"
>
{{ tab.meta.title }}
</n-tag>
<n-dropdown
placement="bottom-start"
trigger="manual"
:x="xAxis"
:y="yAxis"
:options="dropdownOptions"
:show="showDropdown"
:on-clickoutside="() => showDropdown = false"
@select="handleDropdownSelect"
/>
</div>
</template>
<script setup>
import {useTabsStore} from '@/store/modules/tabsStore'
import router from '@/scripts/router'
import {ref, nextTick} from "vue";
import global from "@/scripts/global.js";
const tabsStore = useTabsStore()
const tabs = tabsStore.getTabs
const showDropdown = ref(false);
const dropdownOptions = ref([
{
label: "刷新",
key: 'refresh'
},
{
label: "关闭左侧",
key: 'left'
},
{
label: "关闭右侧",
key: 'right'
},
{
label: "关闭其他",
key: 'other'
}
])
//
const xAxis = ref(0);
const yAxis = ref(0);
function handleClose(path) {
if (tabs.length == 1) {
tabs.splice(0, 1)
router.push({
path: '/home'
})
} else {
tabs.forEach((it, i) => {
if (it.path == path) {
tabs.splice(i, 1)
router.push({
path: tabs[tabs.length - 1].path,
query: tabs[tabs.length - 1].query
})
}
})
}
}
function jump(item) {
router.push({
path: item.path,
query: tabs.filter(it => it.path == item.path)[0].query
})
}
const currentPath = ref()
function handleContextMenu(item, e) {
currentPath.value = item.path
e.preventDefault();
xAxis.value = e.clientX;
yAxis.value = e.clientY;
showDropdown.value = true;
}
function handleDropdownSelect(type) {
if (type != 'refresh') {
close(type)
} else {
tabsStore.refreshReplace({path: currentPath.value})
}
showDropdown.value = false;
}
function close(type) {
let path = currentPath.value
if (type == 'other') {
for (let i = tabs.length - 1; i >= 0; i--) {
if (tabs[i].path != path) {
tabs.splice(i, 1)
}
}
} else if (type == 'right') {
for (let i = tabs.length - 1; i >= 0; i--) {
if (tabs[i].path != path) {
tabs.splice(i, 1)
} else {
break;
}
}
} else {
for (let i = 0, len = tabs.length; i < len; i++) {
if (tabs[0].path != path) {
tabs.splice(0, 1)
} else {
break;
}
}
}
router.push({
path: path,
query: tabs.filter(it => it.path == path)[0].query
})
}
</script>
<style scoped lang="less">
.n-tag {
//padding: 17px 20px;
cursor: pointer;
margin-right: 12px;
border-radius: 4px;
//flex-shrink: 0;
transition: box-shadow;
transition-duration: 0.25s;
}
.small:hover{
box-shadow: 1px 1px 6px #ccc;
}
.medium:hover{
box-shadow: 1px 1px 3px #ccc;
}
.medium{
padding: 15px 20px;
cursor: pointer;
margin-right: 4px;
border-radius: 4px;
}
.small{
padding: 14px 17px;
cursor: pointer;
margin-right: 4px;
border-radius: 4px;
}
.selected {
}
.tabs {
width: 100%;
height: 100%;
overflow-x: auto;
white-space: nowrap;
}
</style>

View File

@ -0,0 +1,37 @@
import './styles/tailwind.css'
import '@/assets/css/common.css'
import 'vite-plugin-svg-icons/register'
import {createApp} from 'vue'
import App from './App.vue'
import {
setupNaive,
setupNaiveDiscreteApi,
setupDirectives,
setupGlobalProperties,
setupMonacoVolar,
setupTheme,
setupLayer
} from '@/scripts/plugins'
import {setupRouter} from '@/scripts/router'
import {setupStore} from '@/store'
import {setupComponents} from '@/components'
import '@/scripts/compiler/magic-import'
const app = createApp(App)
async function start() {
setupStore(app)
setupNaive(app)
setupNaiveDiscreteApi()
setupDirectives(app)
setupGlobalProperties(app)
await setupRouter(app)
await setupMonacoVolar()
setupComponents(app)
await setupTheme()
setupLayer(app)
app.mount('#app')
}
void start()

View File

@ -0,0 +1,376 @@
import request from '@/scripts/request'
import global from '@/scripts/global'
import {utils, writeFile} from 'xlsx'
import {useUserStore} from "@/store/modules/userStore";
import {isArray, cloneDeep} from "lodash-es";
import {h} from 'vue'
import MbIcon from "@/components/magic/basic/mb-icon.vue";
const common = {}
common.handleDelete = (options) => {
const url = options.url
const id = options.id
$dialog.warning({
title: '提示',
content: '此操作将永久删除该数据, 是否继续?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
request({
url: url,
method: 'delete',
params: {
id: id
}
}).then(() => {
$message.success('删除成功')
options && options.done()
})
}
})
}
const formatJson = (list, filterVal) => {
return list.map(v => filterVal.map(j => {
return v[j]
}))
}
common.request = (method) => {
return common[common.requestMethod.indexOf(method) !== -1 ? method : 'get']
}
common.requestMethod = ['get','post','postJson','delete']
common.get = (url, data) => request({url, params: data})
common.delete = (url, data) => request({url, method: 'delete', params: data})
common.post = (url, data) => request.post(url, data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
transformRequest: [data => data && Object.keys(data).map(it => encodeURIComponent(it) + '=' + encodeURIComponent(data[it] === null || data[it] === undefined ? '' : data[it])).join('&')]
})
common.postJson = (url, data) => request.post(url, JSON.stringify(data), {
headers: {
'Content-Type': 'application/json'
}
})
common.renderWhere = (where) => {
let newWhere = {}
for (let key in where) {
if (where[key] instanceof Object) {
newWhere[key] = where[key].value
} else {
newWhere[key] = where[key]
}
}
return newWhere
}
// common.exportExcel = (options) => {
// let where = options.where || {}
// where = common.renderWhere(where)
// where.current = 1
// where.size = 99999999
// const url = options.url
// const headers = options.headers
// const columns = options.columns
// request({
// url: url,
// method: 'post',
// params: where
// }).then(res => {
// import('@/vendor/Export2Excel').then(excel => {
// const data = formatJson(res.data, columns)
// excel.export_json_to_excel({
// header: headers,
// data,
// filename: 'table-list'
// })
// })
// })
// }
common.handlerTreeData = (data, id, pid, sort, pidVal) => {
let treeData = []
let addChildren = (it) => {
let children = data.filter(d => d[pid] === it[id])
if (children && children.length > 0) {
children.sort((a, b) => {
return a[sort] - b[sort]
})
it.children = children
children.forEach(chi => {
addChildren(chi)
})
}
}
data.sort((a, b) => {
return a[sort] - b[sort]
})
data.filter(it => it[pid] === pidVal).forEach(it => {
addChildren(it)
treeData.push(it)
})
return treeData
}
common.uuid = () => {
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}
return (S4() + S4() + S4() + S4() + S4() + S4() + S4() + S4());
}
common.objAssign = (obj1, obj2, exclude) => {
exclude = exclude || ''
for (let o1 in obj1) {
for (let o2 in obj2) {
if (o1 === o2) {
if (exclude.indexOf(o1) == -1) {
obj1[o1] = obj2[o2]
}
}
}
}
}
common.copyNew = (obj) => {
return JSON.parse(JSON.stringify(obj))
}
common.getParam = (data) => {
let url = ''
for (let k in data) {
const value = data[k] !== undefined ? data[k] : ''
url += `&${k}=${encodeURIComponent(value)}`
}
return url ? url.substring(1) : ''
}
common.getUrl = (url, data) => {
url += (url.indexOf('?') < 0 ? '?' : '') + common.getParam(data)
return url
}
function getToken(){
const userStore = useUserStore()
const token = userStore.getToken()
}
common.downloadMore = (urls, filename) => {
let params = {
// post只需编码一次get需要编码两次encodeURIComponent(encodeURIComponent(urls))
urls: encodeURIComponent(urls),
filename: filename || '',
token: getToken()
}
let form = document.createElement("form");
form.style.display = 'none';
form.action = global.baseApi + '/system/file/download';
form.method = 'post';
document.body.appendChild(form);
for(let key in params){
let input = document.createElement("input");
input.type = 'hidden';
input.name = key;
input.value = params[key];
form.appendChild(input);
}
form.submit();
form.remove();
}
common.download = (urls, filename) => {
location.href = common.downloadHref(urls, filename)
}
common.downloadHref = (urls, filename) => {
return global.baseApi + `/system/file/download?urls=${encodeURIComponent(encodeURIComponent(urls))}&filename=${filename || ''}&token=${getToken()}`
}
common.loadConfig = async () => {
await request({
url: '/system/config/list'
}).then(res => {
const {data} = res
global.config = data
global.filePrefix = global.config.bucketDomain ? global.config.bucketDomain : global.baseApi
})
}
common.setDefaultValue = (obj, attr, value) => {
obj[attr] = obj[attr] === undefined ? value : obj[attr]
}
common.isComma = (value) => {
return value.toString().indexOf(',') !== -1
}
common.exportExcel = (options) => {
options.suffix = options.suffix || 'xlsx'
const workBook = utils.json_to_sheet(options.data);
const wb = utils.book_new()
utils.book_append_sheet(wb, workBook, 'sheet1');
writeFile(wb, `${options.fileName}.${options.suffix || 'xlsx'}`);
}
common.objectAssign = (target, source) => {
return Object.assign({}, JSON.parse(JSON.stringify(target)), JSON.parse(JSON.stringify(source)))
}
common.getUrlType = (url) => {
if(!url){
return -1
}
if(url.startsWith('http')){
return 0
}else if(url.indexOf('.htm') != -1){
return 1
}
return 2
}
common.getLocationHref = () => {
return location.href.substring(0, location.href.indexOf('/', location.href.indexOf('/', location.href.indexOf('/') + 1) + 1))
}
common.handlerUrlPage = (url) => {
let urlType = common.getUrlType(url)
if(urlType == 1){
return common.getLocationHref() + url
}else if(urlType == 2){
return common.getLocationHref() + '/#' + url
}
return url
}
common.filterIframeTabs = (it) => {
return it.meta.keepAlive && it.meta.path && (
(it.meta.path.startsWith('http') && (it.meta.openMode == '0' || it.meta.openMode == '2'))
||
(it.meta.path.indexOf('.htm') != -1 && (it.meta.openMode == '0' || it.meta.openMode == '2'))
||
it.meta.openMode == '2'
)
}
/**
* 字符串数组和字符串带逗号对比
*/
common.arrayStringEq = (v1, v2) => {
let value1 = cloneDeep(v1)
let value2 = cloneDeep(v2)
value1 = isArray(value1) ? value1.join(',') : value1 && value1.toString()
value2 = isArray(value2) ? value2.join(',') : value2 && value2.toString()
return value1 == value2
}
/**
* 判断数据不为nullundefined空字符串不包含0和1
*/
common.notEmptyNot01 = (value) => {
if(value !== null && value !== undefined && value !== ''){
return true
}
return false
}
/**
* 获取有效数据 并且 可以设置默认值
*/
common.getValidValue = (value, defaultValue) => {
if(common.notEmptyNot01(value)){
return value
}else{
return common.notEmptyNot01(defaultValue) ? defaultValue : ''
}
}
common.stopWatchList = (watchList) => {
for(let watchFunction of watchList){
watchFunction()
}
}
common.copyText = (selection) => {
let textarea = document.createElement('textarea')
textarea.value = selection
document.body.appendChild(textarea);
textarea.select()
try {
document.execCommand("copy");
$message.success('复制成功')
} catch (err) {
$message.error('复制失败')
}
document.body.removeChild(textarea);
}
common.dialog = (type, options) => {
$dialog[type]({
title: options.title || '提示',
content: options.content,
positiveText: options.positiveText || '确定',
negativeText: options.negativeText || '取消',
onPositiveClick: (e) => {
if(options.ok){
return options.ok(e)
}
},
onNegativeClick: (e) => {
if(options.cancel){
return options.cancel(e)
}
},
...options
})
}
common.warning = (content, ok, options) => {
common.dialog('warning', {
content,
ok,
...options
})
}
common.info = (content, ok, options) => {
common.dialog('info', {
content,
ok,
...options
})
}
common.success = (content, ok, options) => {
common.dialog('success', {
content,
ok,
...options
})
}
common.error = (content, ok, options) => {
common.dialog('error', {
content,
ok,
...options
})
}
common.renderIcon = (icon) => {
return () => h(MbIcon, { icon })
}
common.mapLabelValue = (data, labelField, valueField) => {
return data.map(it => {
return {
label: it[labelField || 'label'],
value: it[valueField || 'value'].toString()
}
})
}
export default common

View File

@ -0,0 +1,18 @@
function appComponent(app, item){
item.compileJs = `(function(){
${item.compileJs}
})()`
let componentStyle = document.createElement("style");
componentStyle.innerHTML = item.compileCss
document.head.appendChild(componentStyle);
app.component(item.name, eval(item.compileJs))
}
export async function loadDynamicComponent(app) {
await $common.post('/system/component/list').then((res) => {
res.data.forEach(it => {
appComponent(app, it)
})
})
}

View File

@ -0,0 +1,30 @@
import * as vue from "vue";
import * as NaiveUI from 'naive-ui';
import * as router from '@/scripts/router'
import xicons from '@/scripts/xicons'
import svgIcons from '@/scripts/svg-icons'
import * as dictStore from "@/store/modules/dictStore";
import * as userStore from "@/store/modules/userStore";
import MbIcon from '@/components/magic/basic/mb-icon.vue';
import * as lodashEs from 'lodash-es'
import * as vueRouter from 'vue-router'
const libs = {
vue,
'naive-ui': NaiveUI,
'@/scripts/xicons': xicons,
'@/scripts/svg-icons': svgIcons,
'@/scripts/router': router,
'@/store/modules/dictStore': dictStore,
'@/store/modules/userStore': userStore,
'@/components/magic/basic/mb-icon.vue': MbIcon,
'lodash-es': lodashEs,
'vue-router': vueRouter
}
window.___magic__import__ = function(lib, name){
if(Object.prototype.toString.call(libs[lib]) != '[object Module]' && name == '*'){
return libs[lib]
}
return (libs[lib] || {})[name]
}

View File

@ -0,0 +1,223 @@
import * as SFCCompiler from '@vue/compiler-sfc'
import {babelParse} from "@vue/compiler-sfc";
const COMP_IDENTIFIER = `__sfc__`
export function compileCode(sourceCode){
let compiled = {}
compileFile(sourceCode, compiled)
if(compiled.errors.length){
throw compiled.errors[0]
}else{
let jsCode = compiled.js
let ast = babelParse(jsCode, {
sourceType: 'module'
})
let replaceCode = (node, subCode) => jsCode.substring(0, node.start) + subCode + jsCode.substring(node.end);
for(let i = ast.program.body.length - 1; i>=0; i--){
let node = ast.program.body[i]
if(node.type === 'ImportDeclaration'){
jsCode = replaceCode(node, node.specifiers.map(it => `const ${it.local?.name || it.imported?.name || '*'} = ___magic__import__('${node.source.value}', '${it.imported?.name || '*'}');`).join('\r\n'));
} else if (node.type === 'ExportDefaultDeclaration'){
jsCode = replaceCode(node, `return ${node.declaration.name}`)
}
}
return {
compileCss: compiled.css,
compileJs: jsCode
}
}
}
export function compileFile(code, compiled) {
const filename = 'mb-sfc-compiler.vue'
const id = hashId(filename)
const { errors, descriptor } = SFCCompiler.parse(code, {
filename,
sourceMap: true
})
if (errors.length) {
compiled.errors = errors
return
}
if(hasVueCompositionFunctions(code) && !hasScriptSetup(code)){
compiled.errors = [
'defineProps、defineExpose、defineEmits、defineSlots、defineOptions、defineModel需要在<script setup>下使用'
]
return
}
if (
(descriptor.script && descriptor.script.lang) ||
(descriptor.scriptSetup && descriptor.scriptSetup.lang) ||
(descriptor.template && descriptor.template.lang)
) {
compiled.errors = [
'lang="x" pre-processors are not supported in the in-browser playground.'
]
return
}
const hasScoped = descriptor.styles.some((s) => s.scoped)
let clientCode = ''
const appendSharedCode = (code) => {
clientCode += code
}
const clientScriptResult = doCompileScript(descriptor, id, compiled)
if (!clientScriptResult) {
return
}
const [clientScript, bindings] = clientScriptResult
clientCode += clientScript
// template
// only need dedicated compilation if not using <script setup>
if (descriptor.template && !descriptor.scriptSetup) {
const clientTemplateResult = doCompileTemplate(
descriptor,
id,
bindings,
compiled
)
if (!clientTemplateResult) {
return
}
clientCode += clientTemplateResult
}
if (hasScoped) {
appendSharedCode(
`\n${COMP_IDENTIFIER}.__scopeId = ${JSON.stringify(`data-v-${id}`)}`
)
}
if (clientCode) {
appendSharedCode(
`\n${COMP_IDENTIFIER}.__file = ${JSON.stringify(filename)}` +
`\nexport default ${COMP_IDENTIFIER}`
)
compiled.js = clientCode.trimStart()
}
// styles
let css = ''
for (const style of descriptor.styles) {
if (style.module) {
compiled.errors = [`<style module> is not supported in the playground.`]
return
}
const styleResult = SFCCompiler.compileStyle({
source: style.content,
filename,
id,
scoped: style.scoped,
modules: !!style.module
})
if (styleResult.errors.length) {
// postcss uses pathToFileURL which isn't polyfilled in the browser
// ignore these errors for now
if (!styleResult.errors[0].message.includes('pathToFileURL')) {
compiled.errors = styleResult.errors
}
// proceed even if css compile errors
} else {
css += styleResult.code + '\n'
}
}
if (css) {
compiled.css = css.trim()
} else {
compiled.css = '/* No <style> tags present */'
}
// clear errors
compiled.errors = []
}
function doCompileScript(descriptor, id, compiled) {
if (descriptor.script || descriptor.scriptSetup) {
try {
const compiledScript = SFCCompiler.compileScript(descriptor, {
id,
refSugar: true,
inlineTemplate: true
})
let code = ''
if (compiledScript.bindings) {
code += `\n/* Analyzed bindings: ${JSON.stringify(
compiledScript.bindings,
null,
2
)} */`
}
code +=
`\n` +
SFCCompiler.rewriteDefault(compiledScript.content, COMP_IDENTIFIER)
// console.log( SFCCompiler.rewriteDefault(compiledScript.content, COMP_IDENTIFIER))
return [code, compiledScript.bindings]
} catch (e) {
compiled.errors = [e]
return
}
} else {
return [`\nconst ${COMP_IDENTIFIER} = {}`, undefined]
}
}
function doCompileTemplate(descriptor, id, bindingMetadata, compiled) {
const templateResult = SFCCompiler.compileTemplate({
source: descriptor.template && descriptor.template.content,
filename: descriptor.filename,
id,
scoped: descriptor.styles.some(s => s.scoped),
slotted: descriptor.slotted,
isProd: false,
compilerOptions: {
bindingMetadata
}
})
if (templateResult.errors.length) {
compiled.errors = templateResult.errors
return
}
const fnName = `render`
return (
`\n${templateResult.code.replace(
/\nexport (function|const) (render|ssrRender)/,
`$1 ${fnName}`
)}` + `\n${COMP_IDENTIFIER}.${fnName} = ${fnName}`
)
}
function hashId(filename) {
return btoa(filename).slice(0, 8)
}
function hasVueCompositionFunctions(content) {
const regexPatterns = [
/defineExpose\s*/,
/defineProps\s*/,
/defineEmits\s*/
];
for (const pattern of regexPatterns) {
if (pattern.test(content)) {
return true;
}
}
return false;
}
function hasScriptSetup(content) {
const regex = /<script\s+setup[^>]*>/;
return regex.test(content);
}

View File

@ -0,0 +1,15 @@
import {useUserStore} from "@/store/modules/userStore";
import { isEmpty } from 'lodash-es'
const permission = {
mounted(el, binding) {
if (binding.value) {
const permissionList = useUserStore().getAuths
if (!isEmpty(permissionList) && !permissionList.includes(binding.value) && import.meta.env.MODE != 'demo') {
el.remove()
}
}
}
}
export default permission

View File

@ -0,0 +1,41 @@
import { ref, reactive } from "vue";
const baseApi = import.meta.env.VITE_APP_BASE_API;
const modalIndex = ref(10)
const modalMap = reactive({})
export default {
title: 'Magic Boot',
baseApi: baseApi,
filePrefix: '',
dynamicComponentNames: [],
config: {},
uiSize: ref('medium'),
selectTheme: {
name: 'default',
themeOverrides: {}
},
themeList: [{
name: 'default',
themeOverrides: 'defaultOverrides',
style: 'defaultStyle'
}],
modal: {
modalMap,
create(id){
modalMap[id] = {}
modalMap[id].vlaue = false
},
show(id){
modalMap[id].value = true
},
hide(id){
modalMap[id].value = false
},
getIndex(id){
modalIndex.value = modalIndex.value + 1
modalMap[id].index = modalIndex.value
return modalIndex.value
}
}
}

View File

@ -0,0 +1,70 @@
/**
* Naive UI Monaco 自动提示
* @author Yean
* @date 2024-3-27 21:19:04
*/
import webTypesData from 'naive-ui/web-types.json'; // 替换成你的web-types.json文件路径
/**
* 获取Naive UI组件自动提示项
* @returns {*[]}
*/
export function naiveUiCompletionItems(monaco) {
const naiveUiComponents = webTypesData.contributions.html['vue-components'];
return naiveUiComponents.flatMap(component => {
const componentName = component.name.replace(/^N/, '').replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); // 去除首字母"N"并转为小写
const tagForm = `<n-${componentName} `;
const componentDescription = component.description;
// 组件自身提示项
const componentCompletion = {
label: tagForm,
kind: monaco.languages.CompletionItemKind.Text,
documentation: componentDescription,
insertText: tagForm,
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
};
// 组件props提示项
const componentProps = component.props.map(prop => ({
label: prop.name,
kind: monaco.languages.CompletionItemKind.Property,
documentation: prop.description,
detail: prop.type,
insertText: `{ ${prop.name}$1 }`, // $1 表示插入点
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
}));
// 组件事件提示项
const componentEvents = component.js?.events?.map(event => ({
label: `@${event.name}`,
kind: monaco.languages.CompletionItemKind.Event,
documentation: event.description || '',
insertText: `@${event.name}="$1"` // $1 表示插入点
}));
// 返回组件、props和事件的CompletionItems
return [componentCompletion, ...componentProps, ...(componentEvents || [])];
});
}
/**
* 注册Naive UI Monaco自动提示
* @param monaco
*/
export function registerNaiveMonacoCompletionProvider(monaco){
monaco.languages.registerCompletionItemProvider('vue', {
triggerCharacters:['<','@','v-on:',' '],
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
};
return { suggestions: naiveUiCompletionItems(monaco) };
}
});
}

View File

@ -0,0 +1,39 @@
import OSS from "ali-oss";
import global from '@/scripts/global'
const init = async () => {
let stsInfo = {}
await $common.get('/oss/sts/service').then(res => {
stsInfo = res.data
})
const client = new OSS({
// yourRegion填写Bucket所在地域。以华东1杭州为例Region填写为oss-cn-hangzhou。
region: 'oss-cn-beijing',
// 从STS服务获取的临时访问密钥AccessKey ID和AccessKey Secret
accessKeyId: stsInfo.accessKeyId,
accessKeySecret: stsInfo.accessKeySecret,
// 从STS服务获取的安全令牌SecurityToken
stsToken: stsInfo.stsToken,
refreshSTSToken: async () => {
// 向您搭建的STS服务获取临时访问凭证。
let info = {}
await $common.get('/oss/sts/service').then(res => {
info = res.data
})
return {
accessKeyId: info.accessKeyId,
accessKeySecret: info.accessKeySecret,
stsToken: info.stsToken
}
},
// 刷新临时访问凭证的时间间隔,单位为毫秒。
refreshSTSTokenInterval: 30000,
// 填写Bucket名称。
bucket: global.config.bucketDomain
});
return client
}
export default {
init
}

View File

@ -0,0 +1,5 @@
import permission from '@/scripts/directives/permission'
export function setupDirectives(app) {
app.directive('permission', permission)
}

View File

@ -0,0 +1,25 @@
import global from '@/scripts/global'
import common from "@/scripts/common";
import treeTable from '@/scripts/treeTable'
export function setupGlobalProperties(app) {
app.config.globalProperties.$global = global
window['$global'] = global
window['$common'] = common
window['$treeTable'] = treeTable
document.body.addEventListener('keyup', (e) => {
if(e.keyCode === 27){ // esc
let maxIndexObject = null;
for (let key in global.modal.modalMap) {
if (global.modal.modalMap[key].value === true) {
if (!maxIndexObject || global.modal.modalMap[key].index > maxIndexObject.index) {
maxIndexObject = global.modal.modalMap[key];
}
}
}
if(maxIndexObject){
maxIndexObject.value = false
}
}
})
}

View File

@ -0,0 +1,7 @@
export {setupNaive} from '@/scripts/plugins/naive'
export {setupNaiveDiscreteApi} from '@/scripts/plugins/naiveDiscreteApi'
export {setupDirectives} from '@/scripts/plugins/directives'
export {setupGlobalProperties} from '@/scripts/plugins/globalProperties'
export {setupMonacoVolar} from '@/scripts/plugins/monacoVolar'
export {setupTheme} from '@/scripts/plugins/theme'
export {setupLayer} from '@/scripts/plugins/layer'

View File

@ -0,0 +1,6 @@
import '@layui/layer-vue/lib/index.css';
import layer from '@layui/layer-vue';
export function setupLayer(app) {
app.use(layer)
}

View File

@ -0,0 +1,57 @@
import * as onigasm from "onigasm";
import onigasmWasm from "onigasm/lib/onigasm.wasm?url";
import * as monaco from "monaco-editor-core";
import * as volar from "@volar/monaco";
import editorWorker from "monaco-editor-core/esm/vs/editor/editor.worker?worker";
import vueWorker from "monaco-volar/vue.worker?worker";
import {registerNaiveMonacoCompletionProvider} from "@/scripts/monaco/naiveui-monaco-prompt";
// 高亮代码
function loadOnigasm() {
return onigasm.loadWASM(onigasmWasm);
}
// 初始化
async function editorInit() {
self.MonacoEnvironment ??= {};
self.MonacoEnvironment.getWorker ??= () => new editorWorker();
const getWorker = self.MonacoEnvironment.getWorker;
self.MonacoEnvironment.getWorker = (_, label) => {
if (label === "vue") {
return new vueWorker();
}
return getWorker();
};
const worker = monaco.editor.createWebWorker({
moduleId: "vs/language/vue/vueWorker",
label: "vue",
createData: {},
});
const languageId = ["vue"];
const getSyncUris = () => monaco.editor.getModels().map((model) => model.uri);
volar.editor.activateMarkers(
worker,
languageId,
"vue",
getSyncUris,
monaco.editor
);
volar.editor.activateAutoInsertion(worker, languageId, getSyncUris, monaco.editor);
await volar.languages.registerProvides(
worker,
languageId,
getSyncUris,
monaco.languages
);
}
export async function setupMonacoVolar(app) {
await loadOnigasm()
monaco.languages.register({id: "vue", extensions: [".vue"]});
monaco.languages.onLanguage("vue", editorInit);
// 注册naive-ui组件自动提示
registerNaiveMonacoCompletionProvider(monaco);
}

View File

@ -0,0 +1,85 @@
import * as NaiveUI from 'naive-ui';
const naive = NaiveUI.create({
components: [
NaiveUI.NMessageProvider,
NaiveUI.NDialogProvider,
NaiveUI.NConfigProvider,
NaiveUI.NInput,
NaiveUI.NInputGroup,
NaiveUI.NInputGroupLabel,
NaiveUI.NButton,
NaiveUI.NForm,
NaiveUI.NFormItem,
NaiveUI.NFormItemGi,
NaiveUI.NCheckboxGroup,
NaiveUI.NCheckbox,
NaiveUI.NIcon,
NaiveUI.NLayout,
NaiveUI.NLayoutHeader,
NaiveUI.NLayoutContent,
NaiveUI.NLayoutFooter,
NaiveUI.NLayoutSider,
NaiveUI.NMenu,
NaiveUI.NBreadcrumb,
NaiveUI.NBreadcrumbItem,
NaiveUI.NDropdown,
NaiveUI.NSpace,
NaiveUI.NTooltip,
NaiveUI.NAvatar,
NaiveUI.NTabs,
NaiveUI.NTabPane,
NaiveUI.NCard,
NaiveUI.NRow,
NaiveUI.NCol,
NaiveUI.NDrawer,
NaiveUI.NDrawerContent,
NaiveUI.NDivider,
NaiveUI.NSwitch,
NaiveUI.NBadge,
NaiveUI.NAlert,
NaiveUI.NElement,
NaiveUI.NTag,
NaiveUI.NNotificationProvider,
NaiveUI.NProgress,
NaiveUI.NDatePicker,
NaiveUI.NGrid,
NaiveUI.NGridItem,
NaiveUI.NList,
NaiveUI.NListItem,
NaiveUI.NThing,
NaiveUI.NDataTable,
NaiveUI.NPopover,
NaiveUI.NPagination,
NaiveUI.NSelect,
NaiveUI.NRadioGroup,
NaiveUI.NRadio,
NaiveUI.NRadioButton,
NaiveUI.NSteps,
NaiveUI.NStep,
NaiveUI.NInputGroup,
NaiveUI.NResult,
NaiveUI.NDescriptions,
NaiveUI.NDescriptionsItem,
NaiveUI.NTable,
NaiveUI.NInputNumber,
NaiveUI.NLoadingBarProvider,
NaiveUI.NModal,
NaiveUI.NUpload,
NaiveUI.NUploadDragger,
NaiveUI.NTree,
NaiveUI.NTreeSelect,
NaiveUI.NSpin,
NaiveUI.NTimePicker,
NaiveUI.NBackTop,
NaiveUI.NSkeleton,
NaiveUI.NImage,
NaiveUI.NImageGroup,
NaiveUI.NSplit,
NaiveUI.NEllipsis
]
});
export function setupNaive(app) {
app.use(naive)
}

View File

@ -0,0 +1,11 @@
import * as NaiveUI from 'naive-ui'
export function setupNaiveDiscreteApi() {
const {message, dialog, notification, loadingBar} = NaiveUI.createDiscreteApi(
['message', 'dialog', 'notification', 'loadingBar']
);
window['$message'] = message;
window['$dialog'] = dialog;
window['$notification'] = notification;
window['$loading'] = loadingBar;
}

Some files were not shown because too many files have changed in this diff Show More