first commit

This commit is contained in:
2026-01-18 09:52:48 +08:00
commit 836bdc9409
584 changed files with 40891 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
<template>
<el-config-provider :locale="locale" :size="config.size" :zIndex="config.zIndex" :button="config.button">
<router-view></router-view>
</el-config-provider>
</template>
<script>
import colorTool from '@/utils/color'
export default {
name: 'App',
data() {
return {
config: {
size: "small",
zIndex: 2000,
button: {
autoInsertSpace: false
}
}
}
},
computed: {
locale(){
return this.$i18n.messages[this.$i18n.locale].el
},
},
created() {
//设置主题颜色
const app_color = this.$CONFIG.COLOR || this.$TOOL.data.get('APP_COLOR')
if(app_color){
document.documentElement.style.setProperty('--el-color-primary', app_color);
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, colorTool.lighten(app_color,i/10));
}
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, colorTool.darken(app_color,i/10));
}
}
const debounce = (fn, delay) => {
let timer = null;
return function () {
let context = this;
let args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
}
}
const _ResizeObserver = window.ResizeObserver;
window.ResizeObserver = class ResizeObserver extends _ResizeObserver{
constructor(callback) {
callback = debounce(callback, 16);
super(callback);
}
}
}
}
</script>
<style lang="scss">
@import '@/assets/style/style.scss';
</style>
+11
View File
@@ -0,0 +1,11 @@
/**
* @description 自动import导入所有 api 模块
*/
const files = import.meta.glob('./module/*.js', { eager: true })
const modules = {}
Object.keys(files).forEach(key => {
modules[key.replace(/^\.\/module\/(.*)\.js$/g, '$1')] = files[key].default
})
export default modules
+153
View File
@@ -0,0 +1,153 @@
import config from "@/config"
import http from "@/utils/request"
export default {
family: {
list: {
url: `${config.API_URL}account/families/index`,
name: "家庭列表",
get: async function(params){
return await http.get(this.url, params);
}
},
detail: {
url: `${config.API_URL}account/families/detail`,
name: "家庭详情",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}account/families/add`,
name: "家庭添加",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}account/families/edit`,
name: "家庭编辑",
post: async function(params){
return await http.put(this.url, params);
}
},
delete: {
url: `${config.API_URL}account/families/delete`,
name: "家庭删除",
post: async function(params){
return await http.delete(this.url, params);
}
}
},
accounts: {
list: {
url: `${config.API_URL}account/accounts/index`,
name: "账户列表",
get: async function(params){
return await http.get(this.url, params);
}
},
detail: {
url: `${config.API_URL}account/accounts/detail`,
name: "账户详情",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}account/accounts/add`,
name: "账户添加",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}account/accounts/edit`,
name: "账户编辑",
post: async function(params){
return await http.put(this.url, params);
}
},
delete: {
url: `${config.API_URL}account/accounts/delete`,
name: "账户删除",
post: async function(params){
return await http.delete(this.url, params);
}
}
},
members: {
list: {
url: `${config.API_URL}account/members/index`,
name: "成员列表",
get: async function(params){
return await http.get(this.url, params);
}
},
detail: {
url: `${config.API_URL}account/members/detail`,
name: "成员详情",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}account/members/add`,
name: "成员添加",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}account/members/edit`,
name: "成员编辑",
post: async function(params){
return await http.put(this.url, params);
}
},
delete: {
url: `${config.API_URL}account/members/delete`,
name: "成员删除",
post: async function(params){
return await http.delete(this.url, params);
}
}
},
records: {
list: {
url: `${config.API_URL}account/records/index`,
name: "记录列表",
get: async function(params){
return await http.get(this.url, params);
}
},
detail: {
url: `${config.API_URL}account/records/detail`,
name: "记录详情",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}account/records/add`,
name: "记录添加",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}account/records/edit`,
name: "记录编辑",
post: async function(params){
return await http.put(this.url, params);
}
},
delete: {
url: `${config.API_URL}account/records/delete`,
name: "记录删除",
post: async function(params){
return await http.delete(this.url, params);
}
}
}
}
+33
View File
@@ -0,0 +1,33 @@
import config from "@/config";
import http from "@/utils/request";
export default {
list: {
url: `${config.API_URL}ads/index`,
name: "获得广告列表",
get: async function (params) {
return await http.get(this.url, params);
},
},
add: {
url: `${config.API_URL}ads/add`,
name: "添加广告",
post: async function (params) {
return await http.post(this.url, params);
},
},
edit: {
url: `${config.API_URL}ads/edit`,
name: "编辑广告",
post: async function (params) {
return await http.put(this.url, params);
},
},
delete: {
url: `${config.API_URL}ads/delete`,
name: "删除广告",
post: async function (params) {
return await http.delete(this.url, params);
},
},
};
+174
View File
@@ -0,0 +1,174 @@
import config from "@/config"
import http from "@/utils/request"
export default {
login: {
url: `${config.API_URL}auth/login`,
name: "登录获取TOKEN",
post: async function(data={}){
return await http.post(this.url, data);
}
},
logout:{
url: `${config.API_URL}auth/logout`,
name: "登出",
get: async function(data={}){
return await http.get(this.url, data);
}
},
user: {
url: `${config.API_URL}auth/user`,
name: "获取当前登录用户",
get: async function(data={}){
return await http.get(this.url, data);
}
},
users: {
list: {
url: `${config.API_URL}auth/users/index`,
name: "获得用户列表",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}auth/users/add`,
name: "添加用户",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}auth/users/edit`,
name: "编辑用户",
post: async function(params){
return await http.put(this.url, params);
}
},
uppasswd:{
url: `${config.API_URL}auth/users/passwd`,
name: "修改密码",
post: async function(params){
return await http.put(this.url, params);
}
},
uprole: {
url: `${config.API_URL}auth/users/uprole`,
name: "设置角色",
post: async function(params){
return await http.put(this.url, params);
}
},
delete: {
url: `${config.API_URL}auth/users/delete`,
name: "删除用户",
post: async function(params){
return await http.delete(this.url, params);
}
}
},
role: {
list: {
url: `${config.API_URL}auth/role/index`,
name: "获得角色列表",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}auth/role/add`,
name: "添加角色",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}auth/role/edit`,
name: "编辑角色",
post: async function(params){
return await http.put(this.url, params);
}
},
auth: {
url: `${config.API_URL}auth/role/auth`,
name: "角色授权",
post: async function(params){
return await http.put(this.url, params);
}
},
delete: {
url: `${config.API_URL}auth/role/delete`,
name: "删除角色",
post: async function(params){
return await http.delete(this.url, params);
}
}
},
department: {
list: {
url: `${config.API_URL}auth/department/index`,
name: "获得部门列表",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}auth/department/add`,
name: "添加部门",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}auth/department/edit`,
name: "编辑部门",
post: async function(params){
return await http.put(this.url, params);
}
},
delete: {
url: `${config.API_URL}auth/department/delete`,
name: "删除部门",
post: async function(params){
return await http.delete(this.url, params);
}
}
},
menu: {
myMenus: {
url: `${config.API_URL}auth/menu/my`,
name: "获取我的菜单",
get: async function(){
return await http.get(this.url);
}
},
list: {
url: `${config.API_URL}auth/menu/index`,
name: "获取菜单",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}auth/menu/add`,
name: "添加菜单",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}auth/menu/edit`,
name: "编辑菜单",
post: async function(params){
return await http.put(this.url, params);
}
},
delete: {
url: `${config.API_URL}auth/menu/delete`,
name: "删除菜单",
post: async function(params){
return await http.delete(this.url, params);
}
}
},
}
+29
View File
@@ -0,0 +1,29 @@
import config from "@/config"
import http from "@/utils/request"
export default {
upload: {
url: `${config.API_URL}system/file/upload`,
name: "文件上传",
post: async function(data, config={}){
return await http.post(this.url, data, config);
}
},
ckeditor: `${config.API_URL}system/file/ckeditor`,
file: {
menu: {
url: `${config.API_URL}system/file/menu`,
name: "获取文件分类",
get: async function(){
return await http.get(this.url);
}
},
list: {
url: `${config.API_URL}system/file/list`,
name: "获取文件列表",
get: async function(params){
return await http.get(this.url, params);
}
}
}
}
+132
View File
@@ -0,0 +1,132 @@
import config from "@/config";
import http from "@/utils/request";
export default {
lists: {
list: {
url: `${config.API_URL}member/index/index`,
name: "获得会员列表",
get: async function (params) {
return await http.get(this.url, params);
},
},
add: {
url: `${config.API_URL}member/index/add`,
name: "添加会员",
post: async function (params) {
return await http.post(this.url, params);
},
},
edit: {
url: `${config.API_URL}member/index/edit`,
name: "编辑会员",
post: async function (params) {
return await http.put(this.url, params);
},
},
delete: {
url: `${config.API_URL}member/index/delete`,
name: "删除会员",
post: async function (params) {
return await http.delete(this.url, params);
},
},
import: {
url: `${config.API_URL}member/index/import`,
name: "导入会员",
post: async function (params) {
return await http.post(this.url, params);
},
},
export: {
url: `${config.API_URL}member/index/export`,
name: "导出会员",
get: async function (params) {
return await http.get(this.url, params);
},
},
},
level: {
list: {
url: `${config.API_URL}member/level/index`,
name: "获得会员等级列表",
get: async function (params) {
return await http.get(this.url, params);
},
},
add: {
url: `${config.API_URL}member/level/add`,
name: "添加会员等级",
post: async function (params) {
return await http.post(this.url, params);
},
},
edit: {
url: `${config.API_URL}member/level/edit`,
name: "编辑会员等级",
post: async function (params) {
return await http.put(this.url, params);
},
},
delete: {
url: `${config.API_URL}member/level/delete`,
name: "删除会员等级",
post: async function (params) {
return await http.delete(this.url, params);
},
},
},
field: {
list: {
url: `${config.API_URL}member/field/index`,
name: "获得会员字段列表",
get: async function (params) {
return await http.get(this.url, params);
},
},
add: {
url: `${config.API_URL}member/field/add`,
name: "添加会员字段",
post: async function (params) {
return await http.post(this.url, params);
},
},
edit: {
url: `${config.API_URL}member/field/edit`,
name: "编辑会员字段",
post: async function (params) {
return await http.put(this.url, params);
},
},
field: {
url: `${config.API_URL}member/field/fields`,
name: "获得会员字段",
get: async function (params) {
return await http.get(this.url, params);
},
},
delete: {
url: `${config.API_URL}member/field/delete`,
name: "删除会员字段",
post: async function (params) {
return await http.delete(this.url, params);
},
},
},
extend: {
list: {
url: `${config.API_URL}member/extend/index`,
name: "获得会员扩展列表",
get: async function (params) {
return await http.get(this.url, params);
},
},
audit: {
url: `${config.API_URL}member/extend/audit`,
name: "申请审核",
post: async function (params) {
return await http.put(this.url, params);
},
},
},
};
+342
View File
@@ -0,0 +1,342 @@
import config from "@/config"
import http from "@/utils/request"
export default {
version:{
url: `${config.API_URL}system/index/version`,
name: "获取最新版本号",
get: async function(){
return await http.get(this.url);
}
},
clearcache: {
url: `${config.API_URL}system/index/clearcache`,
name: "清除缓存",
post: async function(){
return await http.post(this.url);
}
},
info: {
url: `${config.API_URL}system/index/info`,
name: "系统信息",
get: function(data){
return http.get(this.url, data);
}
},
setting:{
list: {
url: `${config.API_URL}system/setting/index`,
name: "获取配置信息",
get: function(params){
return http.get(this.url, params);
}
},
fields: {
url: `${config.API_URL}system/setting/fields`,
name: "获取配置字段",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}system/setting/add`,
name: "保存配置信息",
post: function(data){
return http.post(this.url, data);
}
},
edit: {
url: `${config.API_URL}system/setting/edit`,
name: "编辑配置信息",
post: function(data){
return http.put(this.url, data);
}
},
save: {
url: `${config.API_URL}system/setting/save`,
name: "保存配置信息",
post: function(data){
return http.put(this.url, data);
}
}
},
dictionary: {
category: {
url: `${config.API_URL}system/dict/category`,
name: "获取字典树",
get: async function(params){
return await http.get(this.url, params);
}
},
editcate:{
url: `${config.API_URL}system/dict/editcate`,
name: "编辑字典树",
post: async function(data = {}){
return await http.put(this.url, data);
}
},
addcate:{
url: `${config.API_URL}system/dict/addcate`,
name: "添加字典树",
post: async function(data = {}){
return await http.post(this.url, data);
}
},
delCate:{
url: `${config.API_URL}system/dict/deletecate`,
name: "删除字典树",
post: async function(data = {}){
return await http.delete(this.url, data);
}
},
list: {
url: `${config.API_URL}system/dict/lists`,
name: "字典明细",
get: async function(params){
return await http.get(this.url, params);
}
},
get: {
url: `${config.API_URL}system/dict/detail`,
name: "获取字典数据",
get: async function(params){
return await http.get(this.url, params);
}
},
edit:{
url: `${config.API_URL}system/dict/edit`,
name: "编辑字典明细",
post: async function(data = {}){
return await http.put(this.url, data);
}
},
add:{
url: `${config.API_URL}system/dict/add`,
name: "添加字典明细",
post: async function(data = {}){
return await http.post(this.url, data);
}
},
delete:{
url: `${config.API_URL}system/dict/delete`,
name: "删除字典明细",
post: async function(data = {}){
return await http.delete(this.url, data);
}
},
detail: {
url: `${config.API_URL}system/dict/detail`,
name: "字典明细",
get: async function(params){
return await http.get(this.url, params);
}
},
alldic: {
url: `${config.API_URL}system/dict/all`,
name: "全部字典",
get: async function(params){
return await http.get(this.url, params);
}
}
},
area: {
list: {
url: `${config.API_URL}system/area/index`,
name: "地区列表",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}system/area/add`,
name: "地区添加",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}system/area/edit`,
name: "地区编辑",
post: async function(params){
return await http.put(this.url, params);
}
},
},
app: {
list: {
url: `${config.API_URL}system/app/list`,
name: "应用列表",
get: async function(){
return await http.get(this.url);
}
}
},
client: {
list: {
url: `${config.API_URL}system/client/index`,
name: "客户端列表",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}system/client/add`,
name: "客户端添加",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}system/client/edit`,
name: "客户端编辑",
post: async function(params){
return await http.put(this.url, params);
}
},
delete: {
url: `${config.API_URL}system/client/delete`,
name: "客户端删除",
post: async function(params){
return await http.delete(this.url, params);
}
},
menu: {
list: {
url: `${config.API_URL}system/menu/index`,
name: "客户端菜单列表",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}system/menu/add`,
name: "客户端菜单添加",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}system/menu/edit`,
name: "客户端菜单编辑",
post: async function(params){
return await http.put(this.url, params);
}
},
delete: {
url: `${config.API_URL}system/menu/delete`,
name: "客户端菜单删除",
post: async function(params){
return await http.delete(this.url, params);
}
}
}
},
log: {
list: {
url: `${config.API_URL}system/log/index`,
name: "日志列表",
get: async function(params){
return await http.get(this.url, params);
}
},
my: {
url: `${config.API_URL}system/log/my`,
name: "我的日志",
get: async function(params){
return await http.get(this.url, params);
}
},
delete: {
url: `${config.API_URL}system/log/delete`,
name: "日志删除",
post: async function(params){
return await http.delete(this.url, params);
}
}
},
tasks: {
list: {
url: `${config.API_URL}system/tasks/index`,
name: "任务列表",
get: async function(params){
return await http.get(this.url, params);
}
},
delete: {
url: `${config.API_URL}system/tasks/delete`,
name: "任务删除",
post: async function(params){
return await http.delete(this.url, params);
}
},
},
crontab: {
list: {
url: `${config.API_URL}system/crontab/index`,
name: "定时任务列表",
get: async function(params){
return await http.get(this.url, params);
}
},
add: {
url: `${config.API_URL}system/crontab/add`,
name: "定时任务添加",
post: async function(params){
return await http.post(this.url, params);
}
},
edit: {
url: `${config.API_URL}system/crontab/edit`,
name: "定时任务编辑",
post: async function(params){
return await http.put(this.url, params);
}
},
delete: {
url: `${config.API_URL}system/crontab/delete`,
name: "定时任务删除",
post: async function(params){
return await http.delete(this.url, params);
}
},
log: {
url: `${config.API_URL}system/crontab/log`,
name: "定时任务日志",
get: async function(params){
return await http.get(this.url, params);
}
},
reload: {
url: `${config.API_URL}system/crontab/reload`,
name: "定时任务重载",
post: async function(params){
return await http.put(this.url, params);
}
}
},
modules:{
list: {
url: `${config.API_URL}system/modules/index`,
name: "模块列表",
get: async function(params){
return await http.get(this.url, params);
}
},
update: {
url: `${config.API_URL}system/modules/update`,
name: "更新模块",
post: async function(params){
return await http.post(this.url, params);
}
}
},
sms: {
count: {
url: `${config.API_URL}system/sms/count`,
name: "短信发送统计",
get: async function(params){
return await http.get(this.url, params);
}
}
}
}
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M468.1 320.3V158.7c0.9-8.1-1.3-16.4-7.5-22.7-10.9-10.8-28.4-10.8-39.3 0L147.6 436.1c-5.8 5.7-8.3 13.4-7.9 20.9-0.4 7.5 2.1 15.1 7.9 20.9l272.2 298.4c10.2 8.7 28.8 13.6 40.8 1.6 6.2-6.2 8.9-11.4 8-19.5V594c180.8 0 344.4 130.2 376.8 301.6 21.9-50.2 34.1-105.6 34.1-163.9 0.1-227.2-184.1-411.4-411.4-411.4z" p-id="1450"></path></svg>
</template>
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M258.389333 354.133333a299.093333 299.093333 0 0 1 8.490667-12.8h490.24c2.944 4.181333 5.76 8.490667 8.490667 12.8l86.186666-49.749333 42.666667 73.898667-94.421333 54.528c6.912 25.173333 10.624 51.754667 10.624 79.189333v42.666667h128v85.333333h-128a296.96 296.96 0 0 1-22.869334 114.773333l106.666667 61.610667-42.666667 73.898667-107.776-62.208A298.325333 298.325333 0 0 1 554.666667 935.637333V597.333333h-85.333334v338.346667a298.325333 298.325333 0 0 1-189.354666-107.605333l-107.776 62.208-42.666667-73.898667 106.666667-61.568A297.770667 297.770667 0 0 1 213.333333 640H85.333333v-85.333333h128v-42.666667c0-27.434667 3.712-53.973333 10.624-79.189333L129.536 378.282667l42.666667-73.898667L258.389333 354.133333zM341.333333 256a170.666667 170.666667 0 1 1 341.333334 0H341.333333z" p-id="26147"></path></svg>
</template>
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M554.666667 849.066667a213.418667 213.418667 0 0 0 170.666666-209.066667v-128a212.48 212.48 0 0 0-17.706666-85.333333h-391.253334A212.48 212.48 0 0 0 298.666667 512v128a213.418667 213.418667 0 0 0 170.666666 209.066667V597.333333h85.333334v251.733334z m-318.464-94.293334A297.770667 297.770667 0 0 1 213.333333 640H85.333333v-85.333333h128v-42.666667c0-27.434667 3.712-53.973333 10.624-79.189333L129.536 378.282667l42.666667-73.898667L258.389333 354.133333a299.093333 299.093333 0 0 1 8.490667-12.8h490.24c2.944 4.181333 5.76 8.490667 8.490667 12.8l86.186666-49.749333 42.666667 73.898667-94.421333 54.528c6.912 25.173333 10.624 51.754667 10.624 79.189333v42.666667h128v85.333333h-128a296.96 296.96 0 0 1-22.869334 114.773333l106.666667 61.610667-42.666667 73.898667-107.776-62.208A298.069333 298.069333 0 0 1 512 938.666667a298.069333 298.069333 0 0 1-232.021333-110.592l-107.776 62.208-42.666667-73.898667 106.666667-61.568zM341.333333 256a170.666667 170.666667 0 1 1 341.333334 0H341.333333z" p-id="25873"></path></svg>
</template>
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M981.333333 512l-301.696 301.696-60.330666-60.330667L860.672 512l-241.365333-241.365333 60.330666-60.330667L981.333333 512zM163.328 512l241.365333 241.365333-60.330666 60.330667L42.666667 512l301.696-301.696 60.330666 60.330667L163.328 512z" p-id="4503"></path></svg>
</template>
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M554.666667 426.666667h213.333333l-256 256-256-256h213.333333V128h85.333334v298.666667z m-384 384h682.666666v-298.666667h85.333334v341.333333a42.666667 42.666667 0 0 1-42.666667 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667v-341.333333h85.333334v298.666667z" p-id="26056"></path></svg>
</template>
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M121.984 122.752l536.32-76.586667a21.333333 21.333333 0 0 1 24.362667 21.12v889.429334a21.333333 21.333333 0 0 1-24.32 21.12L121.941333 901.248a42.666667 42.666667 0 0 1-36.650666-42.24V164.992a42.666667 42.666667 0 0 1 36.650666-42.24zM725.333333 128h170.666667a42.666667 42.666667 0 0 1 42.666667 42.666667v682.666666a42.666667 42.666667 0 0 1-42.666667 42.666667h-170.666667V128z m-290.133333 384L554.666667 341.333333h-102.4L384 438.869333 315.733333 341.333333H213.333333l119.466667 170.666667L213.333333 682.666667h102.4L384 585.130667 452.266667 682.666667H554.666667l-119.466667-170.666667z" p-id="3794"></path></svg>
</template>
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M725.333333 128h170.666667a42.666667 42.666667 0 0 1 42.666667 42.666667v682.666666a42.666667 42.666667 0 0 1-42.666667 42.666667h-170.666667V128zM121.984 122.752l536.32-76.586667a21.333333 21.333333 0 0 1 24.362667 21.12v889.429334a21.333333 21.333333 0 0 1-24.32 21.12L121.941333 901.248a42.666667 42.666667 0 0 1-36.650666-42.24V164.992a42.666667 42.666667 0 0 1 36.650666-42.24zM213.333333 341.333333v341.333334h85.333334v-85.333334h256V341.333333H213.333333z m85.333334 85.333334h170.666666v85.333333H298.666667v-85.333333z" p-id="3925"></path></svg>
</template>
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M725.333333 128h170.666667a42.666667 42.666667 0 0 1 42.666667 42.666667v682.666666a42.666667 42.666667 0 0 1-42.666667 42.666667h-170.666667V128zM121.984 122.752l536.32-76.586667a21.333333 21.333333 0 0 1 24.362667 21.12v889.429334a21.333333 21.333333 0 0 1-24.32 21.12L121.941333 901.248a42.666667 42.666667 0 0 1-36.650666-42.24V164.992a42.666667 42.666667 0 0 1 36.650666-42.24zM469.333333 341.333333v212.864L384 469.333333l-84.906667 85.333334L298.666667 341.333333H213.333333v341.333334h85.333334l85.333333-85.333334 85.333333 85.333334h85.333334V341.333333h-85.333334z" p-id="3663"></path></svg>
</template>
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M640 128a42.666667 42.666667 0 0 1 42.666667 42.666667v170.666666a42.666667 42.666667 0 0 1-42.666667 42.666667h-85.333333v85.333333h170.666666a42.666667 42.666667 0 0 1 42.666667 42.666667v128h85.333333a42.666667 42.666667 0 0 1 42.666667 42.666667v170.666666a42.666667 42.666667 0 0 1-42.666667 42.666667h-256a42.666667 42.666667 0 0 1-42.666666-42.666667v-170.666666a42.666667 42.666667 0 0 1 42.666666-42.666667h85.333334v-85.333333H341.333333v85.333333h85.333334a42.666667 42.666667 0 0 1 42.666666 42.666667v170.666666a42.666667 42.666667 0 0 1-42.666666 42.666667H170.666667a42.666667 42.666667 0 0 1-42.666667-42.666667v-170.666666a42.666667 42.666667 0 0 1 42.666667-42.666667h85.333333v-128a42.666667 42.666667 0 0 1 42.666667-42.666667h170.666666V384H384a42.666667 42.666667 0 0 1-42.666667-42.666667V170.666667a42.666667 42.666667 0 0 1 42.666667-42.666667h256zM384 725.333333H213.333333v85.333334h170.666667v-85.333334z m426.666667 0h-170.666667v85.333334h170.666667v-85.333334zM597.333333 213.333333h-170.666666v85.333334h170.666666V213.333333z" p-id="51975"></path></svg>
</template>
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M170.666667 810.666667h682.666666v-298.666667h85.333334v341.333333a42.666667 42.666667 0 0 1-42.666667 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667v-341.333333h85.333334v298.666667z m384-426.666667v298.666667h-85.333334V384H256l256-256 256 256h-213.333333z" p-id="25917"></path></svg>
</template>
+3
View File
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M42.666667 128h170.666666l298.666667 512 298.666667-512h170.666666L512 938.666667 42.666667 128z m369.792 0L512 298.666667l99.541333-170.666667h172.16L512 597.333333 240.298667 128h172.16z" p-id="4634"></path></svg>
</template>
@@ -0,0 +1,3 @@
<template>
<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M792.490667 585.002667a38.826667 38.826667 0 0 0 38.314666-38.314667c0-21.248-17.024-38.314667-38.314666-38.314667s-38.314667 17.066667-38.314667 38.314667c0 21.333333 17.066667 38.314667 38.314667 38.314667z m-188.8 0a38.826667 38.826667 0 0 0 38.314666-38.314667c0-21.248-17.066667-38.314667-38.314666-38.314667-21.333333 0-38.314667 17.066667-38.314667 38.314667 0 21.333333 17.024 38.314667 38.314667 38.314667z m280.192 215.04a14.805333 14.805333 0 0 0-7.338667 15.786666c0 2.048 0 4.138667 1.066667 6.272 4.181333 17.792 12.544 46.122667 12.544 47.189334 0 3.114667 1.066667 5.205333 1.066666 7.338666a9.386667 9.386667 0 0 1-9.429333 9.386667c-2.133333 0-3.157333-1.024-5.248-2.048l-61.824-35.669333a34.090667 34.090667 0 0 0-14.677333-4.181334c-3.114667 0-6.272 0-8.362667 1.024-29.354667 8.405333-59.733333 12.586667-92.202667 12.586667-156.16 0-281.898667-104.832-281.898666-234.88 0-130.005333 125.738667-234.88 281.898666-234.88 156.117333 0 281.856 104.874667 281.856 234.88 0 70.272-37.717333 134.229333-97.450666 177.237333zM711.381333 345.557333a388.48 388.48 0 0 0-11.946666-0.213333c-178.090667 0-324.522667 122.026667-324.522667 277.546667 0 23.637333 3.413333 46.506667 9.728 68.266666h-3.797333a425.088 425.088 0 0 1-110.250667-15.701333c-3.157333-1.066667-6.314667-1.066667-9.472-1.066667a35.498667 35.498667 0 0 0-17.834667 5.248l-74.581333 42.88c-2.133333 1.066667-4.224 2.133333-6.314667 2.133334a11.648 11.648 0 0 1-11.52-11.52c0-3.157333 1.024-5.248 2.090667-8.405334 1.024-1.024 10.496-35.584 15.744-56.490666 0-2.133333 1.024-5.248 1.024-7.338667a23.722667 23.722667 0 0 0-9.429333-18.858667C87.808 570.709333 42.666667 494.336 42.666667 409.514667 42.666667 253.653333 194.986667 128 381.866667 128c160.64 0 295.68 92.544 329.514666 217.514667z m-219.904 17.834667c24.448 0 43.776-20.352 43.776-43.776 0-24.448-19.328-43.776-43.776-43.776s-43.776 19.328-43.776 43.776 19.328 43.776 43.776 43.776z m-224.426666 0c24.448 0 43.818667-20.352 43.818666-43.776 0-24.448-19.370667-43.776-43.818666-43.776-24.405333 0-43.776 19.328-43.776 43.776s19.370667 43.776 43.776 43.776z" p-id="4372"></path></svg>
</template>
+12
View File
@@ -0,0 +1,12 @@
export { default as Vue } from './Vue.vue'
export { default as Code } from './Code.vue'
export { default as Wechat } from './Wechat.vue'
export { default as BugFill } from './BugFill.vue'
export { default as BugLine } from './BugLine.vue'
export { default as FileWord } from './FileWord.vue'
export { default as FileExcel } from './FileExcel.vue'
export { default as FilePpt } from './FilePpt.vue'
export { default as Organization } from './Organization.vue'
export { default as Upload } from './Upload.vue'
export { default as Download } from './Download.vue'
export { default as Back } from './Back.vue'
+103
View File
@@ -0,0 +1,103 @@
/* 全局 */
#app, body, html {width: 100%;height: 100%;background-color: #f6f8f9;font-size: 12px;}
a {color: #333;text-decoration: none;}
a:hover, a:focus {color: #000;text-decoration: none;}
a:link {text-decoration: none;}
a:-webkit-any-link {text-decoration: none;}
a,button,input,textarea{-webkit-tap-highlight-color:rgba(0,0,0,0);box-sizing: border-box;outline:none !important; -webkit-appearance: none;}
* {margin: 0;padding: 0;box-sizing: border-box;outline: none;}
/* 大布局样式 */
.aminui {display: flex;flex-flow: column;}
.aminui-wrapper {display: flex;flex:1;overflow: auto;}
/* 全局滚动条样式 */
.scrollable {-webkit-overflow-scrolling: touch;}
::-webkit-scrollbar {width: 5px;height: 5px;}
::-webkit-scrollbar-thumb {background-color: rgba(50, 50, 50, 0.3);}
::-webkit-scrollbar-thumb:hover {background-color: rgba(50, 50, 50, 0.6);}
::-webkit-scrollbar-track {background-color: rgba(50, 50, 50, 0.1);}
::-webkit-scrollbar-track:hover {background-color: rgba(50, 50, 50, 0.2);}
/*布局设置*/
.layout-setting {position: fixed;width: 40px;height: 40px;border-radius: 3px 0 0 3px;bottom: 100px;right: 0px;z-index: 100;background: #409EFF;display: flex;flex-direction: column;align-items: center;justify-content: center;cursor: pointer;}
.layout-setting i {font-size: 18px;color: #fff;}
/* 头部 */
.adminui-header {height: 58px;background: #fff;color: #222b45; border-bottom: 1px solid rgba(0, 0, 0, 0.05); display: flex;justify-content:space-between;}
.adminui-header-left {display: flex;align-items: center;padding-left:20px;}
.adminui-header-right {display: flex;align-items: center;}
.adminui-header .logo-bar {font-size: 20px;font-weight: bold;display: flex;align-items: center;}
.adminui-header .logo-bar .logo {margin-right: 10px;height: 35px;}
.adminui-header .nav {display: flex;height: 100%;margin-left: 40px;}
.adminui-header .nav li {padding:0 10px;margin: 0 10px 0 0;font-size: 14px;color: rgba(255, 255, 255, 0.6);list-style: none;height: 100%;display: flex;align-items: center;cursor: pointer;}
.adminui-header .nav li i {margin-right: 5px;}
.adminui-header .nav li:hover {color: #222b45;}
.adminui-header .nav li.active {background: rgba(255, 255, 255, 0.1);color: #222b45;}
.adminui-header .user-bar .panel-item:hover {background: rgba(255, 255, 255, 0.1)!important;}
.adminui-header .user-bar .user label{color: #222b45;}
/* 左侧菜单 */
.aminui-side-split {width:65px;flex-shrink:0;background: #222b45;display: flex;flex-flow: column;}
.aminui-side-split-top {height: 49px;}
.aminui-side-split-top a {display: inline-block;width: 100%;height: 100%;display: flex;align-items: center;justify-content: center;}
.aminui-side-split-top .logo {height:30px;vertical-align: bottom;}
.adminui-side-split-scroll {overflow: auto;overflow-x:hidden;height: 100%;flex: 1;}
.aminui-side-split li {cursor: pointer;width: 65px;height: 65px;color: #fff;text-align: center;display: flex;flex-direction: column;align-items: center;justify-content: center;}
.aminui-side-split li i {font-size: 18px;}
.aminui-side-split li p {margin-top:5px;}
.aminui-side-split li:hover {background: rgba(255, 255, 255, 0.1);}
.aminui-side-split li.active {background: #409EFF;}
.adminui-side-split-scroll::-webkit-scrollbar-thumb {background-color: rgba(255, 255, 255, 0.4);border-radius:5px;}
.adminui-side-split-scroll::-webkit-scrollbar-thumb:hover {background-color: rgba(255, 255, 255, 0.5);}
.adminui-side-split-scroll::-webkit-scrollbar-track {background-color: rgba(255, 255, 255, 0);}
.adminui-side-split-scroll::-webkit-scrollbar-track:hover {background-color: rgba(255, 255, 255, 0);}
.aminui-side {display: flex;flex-flow: column;flex-shrink:0;width:210px;background: #fff;box-shadow: 2px 0 8px 0 rgba(29,35,41,.05);border-right: 1px solid #e6e6e6;transition:width 0.3s;}
.adminui-side-top {border-bottom: 1px solid #ebeef5;height:50px;line-height: 50px;}
.adminui-side-top h2 {padding:0 20px;font-size: 17px;color: #3c4a54;}
.adminui-side-scroll {overflow: auto;overflow-x:hidden;flex: 1;}
.adminui-side-bottom {border-top: 1px solid #ebeef5;height:51px;cursor: pointer;display: flex;align-items: center;justify-content: center;}
.adminui-side-bottom i {font-size: 16px;}
.adminui-side-bottom:hover {color: var(--el-color-primary);}
.aminui-side.isCollapse {width: 65px;}
.el-menu .menu-tag {position: absolute;height: 18px;line-height: 18px;background: var(--el-color-danger);font-size: 12px;color: #fff;right: 20px;border-radius:18px;padding:0 6px;}
.el-menu .el-sub-menu__title .menu-tag {right: 40px;}
.el-menu--horizontal > li .menu-tag {display: none;}
/* 右侧内容 */
.aminui-body {flex: 1;display: flex;flex-flow: column;}
.adminui-topbar {height: 50px;border-bottom: 1px solid #ebeef5;background: #fff;box-shadow: 0 1px 4px rgba(0,21,41,.08);display: flex;justify-content:space-between;}
.adminui-topbar .left-panel {display: flex;align-items: center;}
.adminui-topbar .right-panel {display: flex;align-items: center;}
.right-panel-search {display: flex;align-items: center;}
.right-panel-search > * + * {margin-left:10px;}
.adminui-tags {height:35px;background: #fff;border-bottom: 1px solid #e6e6e6;}
.adminui-tags ul {display: flex;overflow: hidden;}
.adminui-tags li {cursor: pointer;display: inline-block;float: left;height:34px;line-height: 34px;position: relative;flex-shrink: 0;}
.adminui-tags li::after {content: " ";width:1px;height:100%;position: absolute;right:0px;background-image: linear-gradient(#fff, #e6e6e6);}
.adminui-tags li a {display: inline-block;padding:0 10px;width:100%;height:100%;color: #999;text-decoration:none;display: flex;align-items: center;}
.adminui-tags li i {margin-left:10px;border-radius: 3px;width:18px;height:18px;display: flex;align-items: center;justify-content: center;}
.adminui-tags li i:hover {background: rgba(0,0,0,.2);color: #fff;}
.adminui-tags li:hover {background: #ecf5ff;}
.adminui-tags li.active {background: #409EFF;}
.adminui-tags li.active a {color: #fff;}
.adminui-tags li.sortable-ghost {opacity: 0;}
.adminui-main {overflow: auto;background-color: #f6f8f9;flex: 1;}
/*页面最大化*/
.aminui.main-maximize {
.main-maximize-exit {display: block;}
.aminui-side-split, .aminui-side, .adminui-header, .adminui-topbar, .adminui-tags {display: none;}
}
.main-maximize-exit {display: none;position: fixed;z-index: 3000;top:-20px;left:50%;margin-left: -20px;border-radius: 50%;width: 40px;height: 40px;cursor: pointer;background: rgba(0,0,0,0.2);text-align: center;}
.main-maximize-exit i {font-size: 14px;margin-top: 22px;color: #fff;}
.main-maximize-exit:hover {background: rgba(0,0,0,0.4);}
/*定宽页面*/
.sc-page {width: 1230px;margin: 0 auto;}
@@ -0,0 +1,37 @@
@import 'element-plus/theme-chalk/src/dark/css-vars.scss';
html.dark {
//变量
--el-text-color-primary: #d0d0d0;
--el-color-primary-dark-2: var(--el-color-primary-light-2) !important;
--el-color-primary-light-9: var(--el-color-primary-dark-8) !important;
--el-color-primary-light-8: var(--el-color-primary-dark-7) !important;
--el-color-primary-light-7: var(--el-color-primary-dark-6) !important;
--el-color-primary-light-5: var(--el-color-primary-dark-4) !important;
--el-color-primary-light-3: var(--el-color-primary-dark-3) !important;
//背景
#app {background: var(--el-bg-color);}
//登录背景
.login_bg {background: var(--el-bg-color);}
//框架
.adminui-header {background: var(--el-bg-color-overlay);border-bottom: 1px solid var(--el-border-color-light);height:59px;}
.aminui-side-split {background: var(--el-bg-color);}
.aminui-side-split li {color: var(--el-text-color-primary);}
.aminui-side {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
.adminui-side-top, .adminui-side-bottom {border-color: var(--el-border-color-light);}
.adminui-side-top h2 {color: var(--el-text-color-primary);}
.adminui-topbar, .adminui-tags {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
.adminui-main {background: var(--el-bg-color);}
.drawerBG {background: var(--el-bg-color);}
.adminui-header-menu .el-menu {--el-menu-bg-color:var(--el-bg-color-overlay) !important;--el-menu-hover-bg-color: #171819 !important;}
//组件
.el-header, .el-main.nopadding, .el-footer {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
.el-main {background: var(--el-bg-color);}
.el-aside {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
.el-table .el-table__body-wrapper {background: var(--el-bg-color);}
.el-table th.is-sortable:hover {background: #111;}
}
+12
View File
@@ -0,0 +1,12 @@
input::-webkit-outer-spin-button,input::-webkit-inner-spin-button { -webkit-appearance: none;}
.action-icon{cursor: pointer; width: 1.2em; height: 1.2em; color: #409EFC; vertical-align: middle; margin-right: 5px;}
.el-tabs__new-tab{margin: 10px;}
.el-drawer__header{margin-bottom: 10px;}
.el-card__header{padding: 10px; padding-bottom: 0; font-size: 14px;}
.el-card__body{padding: 10px;}
.el-dialog__body{padding: calc(var(--el-dialog-padding-primary)) var(--el-dialog-padding-primary);}
.el-upload-dragger, .el-upload-dragger.is-dragover{padding: 0; border: none;}
+83
View File
@@ -0,0 +1,83 @@
/* 覆盖element-plus样式 */
:root {
--el-color-primary: #409EFF;
--el-color-primary-light-1: #53a7ff;
--el-color-primary-light-2: #66b1ff;
--el-color-primary-light-3: #79bbff;
--el-color-primary-light-4: #8cc4ff;
--el-color-primary-light-5: #9fceff;
--el-color-primary-light-6: #b2d8ff;
--el-color-primary-light-7: #c5e1ff;
--el-color-primary-light-8: #d8ebff;
--el-color-primary-light-9: #ebf5ff;
--el-color-primary-dark-1: #398ee5;
--el-color-primary-dark-2: #337ecc;
--el-color-primary-dark-3: #2c6eb2;
--el-color-primary-dark-4: #265e99;
--el-color-primary-dark-5: #204f7f;
--el-color-primary-dark-6: #193f66;
--el-color-primary-dark-7: #132f4c;
--el-color-primary-dark-8: #0c1f32;
--el-color-primary-dark-9: #060f19;
}
.el-menu {border: none!important;}
.el-menu .el-menu-item a {color: inherit;text-decoration: none;display: block;width:100%;height:100%;position: absolute;top:0px;left:0px;}
.el-form-item-msg {font-size: 12px;color: #999;clear: both;width: 100%;}
.el-container {height: 100%;}
.el-aside {border-right: 1px solid var(--el-border-color-light);}
.el-container + .el-aside {border-right: 0;border-left: 1px solid var(--el-border-color-light);}
.el-header {background: #fff;border-bottom: 1px solid var(--el-border-color-light);padding:13px 15px;display: flex;justify-content: space-between;align-items: center;}
.el-header .left-panel {display: flex;align-items: center;}
.el-header .right-panel {display: flex;align-items: center;}
.el-header .right-panel > * + * {margin-left:10px;}
.el-footer {background: #fff;border-top: 1px solid var(--el-border-color-light);padding:13px 15px;height: 51px;}
.el-main {padding:15px;}
.el-main.nopadding {padding:0;background: #fff;}
.el-drawer__body {overflow: auto;padding:0;}
.el-popconfirm__main {margin: 14px 0;}
.el-card__header {border-bottom: 0;font-size: 17px;font-weight: bold;padding:15px 20px 0px 20px;}
.el-dialog__title {font-size: 17px;font-weight: bold;}
.el-drawer__header>:first-child {font-size: 17px;font-weight: bold;}
.el-tree.menu .el-tree-node__content {height:36px;}
.el-tree.menu .el-tree-node__content .el-tree-node__label .icon {margin-right: 5px;}
.el-progress__text {font-size: 12px!important;}
.el-progress__text i {font-size: 14.4px!important;}
.el-step.is-horizontal .el-step__line {height:1px;}
.el-step__title {font-size: 14px;}
.drawerBG {background: #f6f8f9;}
.el-button+.el-dropdown {margin-left: 10px;}
.el-button-group+.el-dropdown {margin-left: 10px;}
.el-tag+.el-tag {margin-left: 10px;}
.el-button-group+.el-button-group {margin-left: 10px;}
.el-tabs__nav-wrap::after {height: 1px;}
.el-table th.is-sortable {transition: .1s;}
.el-table th.is-sortable:hover {background: #eee;}
.el-table .el-table__body-wrapper {background: #f6f8f9;}
.el-col .el-card {margin-bottom: 15px;}
.el-main {flex-basis: 100%;}
.el-main > .scTable .el-table--border::before {display: none;}
.el-main > .scTable .el-table--border::after {display: none;}
.el-main > .scTable .el-table--border .el-table__inner-wrapper::after {display: none;}
.el-main > .scTable .el-table__border-left-patch {display: none;}
.el-main > .scTable .el-table--border .el-table__inner-wrapper tr:first-child td:first-child {border-left: 0;}
.el-main > .scTable .el-table--border .el-table__inner-wrapper tr:first-child th:first-child {border-left: 0;}
.el-table.el-table--large {font-size: 14px;}
.el-table.el-table--small {font-size: 12px;}
.el-table {font-size: 12px;}
.el-radio-button__inner {font-size: 12px;}
.el-checkbox-button__inner {font-size: 12px;}
.el-sub-menu .el-icon {font-size: 17px;}
.el-sub-menu .el-sub-menu__icon-arrow {font-size: 12px;}
.aminui-side-split li.active {background-color: var(--el-color-primary);}
.adminui-tags li:hover {background-color: var(--el-color-primary-light-9);}
.adminui-tags li.active {background-color: var(--el-color-primary)!important;}
.contextmenu li:hover {background-color: var(--el-color-primary-light-9)!important;color: var(--el-color-primary-light-2)!important;}
.data-box .item-background {background-color: var(--el-color-primary)!important;}
.layout-setting,.diy-grid-setting {background-color: var(--el-color-primary)!important;}
/* 覆盖tinymce样式 */
.sceditor .tox-tinymce {border: 1px solid #DCDFE6;border-radius: 0;}
body .tox-tinymce-aux {z-index: 5700;}
@@ -0,0 +1,50 @@
@media (max-width: 992px){
// 移动端样式覆盖
.el-form-item {display: block;}
.el-form-item__label {display: block;text-align: left;padding: 0 0 10px;}
.el-dialog {width: 90%!important;}
.el-dialog.is-fullscreen {width: 100%!important;}
.el-drawer.rtl {width: 90%!important;}
.el-form-item__content {margin-left: 0px!important;}
.adminui-main {
>.el-container {display: block;height:auto;}
>.el-container > .el-aside {width: 100%!important;border: 0}
}
.scTable {
.el-table,
.el-table__body-wrapper {display: block!important;height:auto!important;}
.el-scrollbar__wrap {height:auto!important;}
.scTable-page {padding: 0 5px!important;}
.el-pagination__total,
.el-pagination__jump,
.el-pagination__sizes {display: none!important;}
}
.headerPublic {
height: auto!important;display: block;
.left-panel {overflow: auto;}
.left-panel::-webkit-scrollbar{display: none;}
.right-panel {display: block;border-top: 1px solid var(--el-border-color-light);margin-top: 15px;}
.right-panel .right-panel-search {display: block;}
.right-panel .right-panel-search >* {width: 100%;margin: 0;margin-top: 15px;}
}
.adminui-main > .el-container >*:first-child:not(.el-aside):not(.el-header) {border: 0;margin-top: 0;}
.adminui-main > .el-container >*:first-child:not(.el-aside):not(.el-header) + .el-aside {margin-top: 0;}
.adminui-main > .el-container > .el-aside {border-bottom: 1px solid var(--el-border-color-light)!important;}
.adminui-main > .el-container > .el-container {border-top: 1px solid var(--el-border-color-light);border-bottom: 1px solid var(--el-border-color-light);margin-top: 15px;}
.adminui-main > .el-container > .el-container + .el-aside {border-top: 1px solid var(--el-border-color-light);margin-top: 15px;}
.adminui-main > .el-container > .el-header {@extend .headerPublic;}
.adminui-main > .el-container > .el-main.nopadding {border-top: 1px solid var(--el-border-color-light);border-bottom: 1px solid var(--el-border-color-light);margin-top: 15px;}
.adminui-main > .el-container > .el-main + .el-aside {border-left: 0!important;border-top: 1px solid var(--el-border-color-light);margin-top: 15px;}
.adminui-main > .el-container > .el-footer {margin-top: 15px;border-bottom: 1px solid var(--el-border-color-light);}
.adminui-main > .el-container > .el-container > .el-header {@extend .headerPublic}
.adminui-main > .el-container > .el-container > .el-header .left-panel {display: block;}
.adminui-main > .el-container > .el-container > .el-header .right-panel {display: block;margin-top: 15px;}
.sc-page {width: 100%;margin: 0;}
.common-main .el-form {width: 100% !important;}
.common-header-logo label {display: none;}
.common-header-title {display: none;}
}
@@ -0,0 +1,44 @@
/* USERCENTER */
.page-user {
.user-info-top {text-align: center;}
.user-info-top h2 {font-size: 18px;margin-top: 5px;}
.user-info-top p {margin: 8px 0 10px 0;}
.menu {background: none;}
.menu .el-menu-item {font-size: 12px;--el-menu-item-height:50px;}
.menu .el-menu-item-group {border-top: 1px solid var(--el-border-color-light);}
.menu .el-menu-item-group:first-child {border: 0;}
}
/*static-table*/
.static-table {border-collapse: collapse;width: 100%;font-size: 14px;margin-bottom: 45px;line-height: 1.5em;}
.static-table th {text-align: left;white-space: nowrap;color: #909399;font-weight: 400;border-bottom: 1px solid #dcdfe6;padding: 15px;max-width: 250px;}
.static-table td {border-bottom: 1px solid #dcdfe6;padding: 15px;max-width: 250px;color: #606266;}
/*header-tabs*/
.header-tabs {padding:10px 0 0 0;display:block;border:0!important;height:50px;background: none;}
.header-tabs .el-tabs__header {padding-left:10px;margin: 0;}
.header-tabs .el-tabs__content {display: none;}
.header-tabs .el-tabs__nav {border-radius: 0 !important;}
.header-tabs .el-tabs__item {font-size: 13px;}
.header-tabs .el-tabs__item.is-active {background-color: var(--el-bg-color-overlay);}
/*common-page*/
.common-page {}
.common-header-left {display: flex;align-items: center;}
.common-header-logo {display: flex;align-items: center;}
.common-header-logo img {height:30px;margin-right: 10px;vertical-align: bottom;}
.common-header-logo label {font-size: 20px;}
.common-header-title {font-size: 16px;border-left: 1px solid var(--el-border-color-light);margin-left: 15px;padding-left: 15px;}
.common-header-right {display: flex;align-items: center;}
.common-header-right a {font-size: 14px;color: var(--el-color-primary);cursor: pointer;}
.common-header-right a:hover {color: var(--el-color-primary-light-3);}
.common-container {max-width: 1240px;margin:30px auto 30px auto;}
.common-main {padding:20px;}
.common-title {font-size: 26px;margin-bottom: 20px;font-weight: normal;}
.common-main .el-form {width: 500px;margin:30px auto;}
.common-main .el-steps .el-step__title {font-size: 14px;}
.common-main .el-steps .el-step__icon {border: 1px solid;}
.common-main .yzm {display: flex;width: 100%;}
.common-main .yzm .el-button {margin-left: 10px;}
.common-main .link {color: var(--el-color-primary);cursor: pointer;}
.common-main .link:hover {color: var(--el-color-primary-light-3);}
@@ -0,0 +1,6 @@
@import 'app.scss';
@import 'fix.scss';
@import 'pages.scss';
@import 'media.scss';
@import 'dark.scss';
@import 'diy.scss';
@@ -0,0 +1,115 @@
<!--
* @Descripttion: 代码编辑器
* @version: 1.0
* @Author: sakuya
* @Date: 2022年5月20日21:46:29
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-code-editor" :style="{'height':_height}">
<textarea ref="textarea" v-model="contentValue"></textarea>
</div>
</template>
<script>
import { markRaw } from "vue"
//框架
import CodeMirror from 'codemirror'
import 'codemirror/lib/codemirror.css'
//主题
import 'codemirror/theme/idea.css'
import 'codemirror/theme/darcula.css'
//功能
import 'codemirror/addon/selection/active-line'
//语言
import 'codemirror/mode/javascript/javascript'
import 'codemirror/mode/sql/sql'
export default {
props: {
modelValue: {
type: String,
default: ""
},
mode: {
type: String,
default: "javascript"
},
height: {
type: [String,Number],
default: 300,
},
options: {
type: Object,
default: () => {}
},
theme: {
type: String,
default: "idea"
},
readOnly: {
type: Boolean,
default: false
},
},
data() {
return {
contentValue: this.modelValue,
coder: null,
opt: {
theme: this.theme, //主题
styleActiveLine: true, //高亮当前行
lineNumbers: true, //行号
lineWrapping: false, //自动换行
tabSize: 4, //Tab缩进
indentUnit: 4, //缩进单位
indentWithTabs : true, //自动缩进
mode : this.mode, //语言
readOnly: this.readOnly, //只读
...this.options
}
}
},
computed: {
_height() {
return Number(this.height)?Number(this.height)+'px':this.height
},
},
watch: {
modelValue(val) {
this.contentValue = val
if (val !== this.coder.getValue()) {
this.coder.setValue(val)
}
}
},
mounted() {
this.init()
//获取挂载的所有modes
//console.log(CodeMirror.modes)
},
methods: {
init(){
this.coder = markRaw(CodeMirror.fromTextArea(this.$refs.textarea, this.opt))
this.coder.on('change', (coder) => {
this.contentValue = coder.getValue()
this.$emit('update:modelValue', this.contentValue)
})
},
formatStrInJson(strValue) {
return JSON.stringify(JSON.parse(strValue), null, 4)
}
}
}
</script>
<style scoped>
.sc-code-editor {font-size: 14px;border: 1px solid #ddd;line-height: 150%;}
.sc-code-editor:deep(.CodeMirror) {height: 100%;}
</style>
@@ -0,0 +1,100 @@
<!--
* @Descripttion: scContextmenu组件
* @version: 1.1
* @Author: sakuya
* @Date: 2021年7月23日09:25:57
* @LastEditors: sakuya
* @LastEditTime: 2022年5月30日20:17:42
* @other: 代码完全开源欢迎参考也欢迎PR
-->
<template>
<transition name="el-zoom-in-top">
<div v-if="visible" ref="contextmenu" class="sc-contextmenu" :style="{left:left+'px',top:top+'px'}" @contextmenu.prevent="fun">
<ul class="sc-contextmenu__menu">
<slot></slot>
</ul>
</div>
</transition>
</template>
<script>
export default {
provide() {
return {
menuClick: this.menuClick
}
},
data() {
return {
visible: false,
top: 0,
left: 0
}
},
watch: {
visible(value) {
if (value) {
document.body.addEventListener('click', this.cm, true)
}else{
document.body.removeEventListener('click', this.cm, true)
}
}
},
mounted() {
},
methods: {
cm(e){
let sp = this.$refs.contextmenu
if(sp&&!sp.contains(e.target)){
this.closeMenu()
}
},
menuClick(command){
this.closeMenu()
this.$emit('command', command)
},
openMenu(e) {
e.preventDefault()
this.visible = true
this.left = e.clientX + 1
this.top = e.clientY + 1
this.$nextTick(() => {
var ex = e.clientX + 1
var ey = e.clientY + 1
var innerWidth = window.innerWidth
var innerHeight = window.innerHeight
var menuHeight = this.$refs.contextmenu.offsetHeight
var menuWidth = this.$refs.contextmenu.offsetWidth
//位置修正公示
//left = (当前点击X + 菜单宽度 > 可视区域宽度 ? 可视区域宽度 - 菜单宽度 : 当前点击X)
//top = (当前点击Y + 菜单高度 > 可视区域高度 ? 当前点击Y - 菜单高度 : 当前点击Y)
this.left = ex + menuWidth > innerWidth ? innerWidth - menuWidth : ex
this.top = ey + menuHeight > innerHeight ? ey - menuHeight : ey
})
this.$emit('visibleChange', true)
},
closeMenu() {
this.visible = false;
this.$emit('visibleChange', false)
},
fun(){
return false;
}
}
}
</script>
<style>
.sc-contextmenu {position: fixed;z-index: 3000;font-size: 12px;}
.sc-contextmenu__menu {display: inline-block;min-width: 120px;border: 1px solid #e4e7ed;background: #fff;box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);z-index: 3000;list-style-type: none;padding: 10px 0;}
.sc-contextmenu__menu > hr {margin:5px 0;border: none;height: 1px;font-size: 0px;background-color: #ebeef5;}
.sc-contextmenu__menu > li {margin:0;cursor: pointer;line-height: 30px;padding: 0 17px 0 10px;color: #606266;display: flex;justify-content: space-between;white-space: nowrap;text-decoration: none;position: relative;}
.sc-contextmenu__menu > li:hover {background-color: #ecf5ff;color: #66b1ff;}
.sc-contextmenu__menu > li.disabled {cursor: not-allowed;color: #bbb;background: transparent;}
.sc-contextmenu__icon {display: inline-block;width: 14px;font-size: 14px;margin-right: 10px;}
.sc-contextmenu__suffix {margin-left: 40px;color: #999;}
.sc-contextmenu__menu li ul {position: absolute;top:0px;left:100%;display: none;margin: -11px 0;}
</style>
@@ -0,0 +1,84 @@
<!--
* @Descripttion: scContextmenuItem组件
* @version: 1.2
* @Author: sakuya
* @Date: 2021年7月23日16:29:36
* @LastEditors: sakuya
* @LastEditTime: 2022年2月8日15:51:07
-->
<template>
<hr v-if="divided">
<li :class="disabled?'disabled':''" @click.stop="liClick" @mouseenter="openSubmenu($event)" @mouseleave="closeSubmenu($event)">
<span class="title">
<el-icon class="sc-contextmenu__icon"><component v-if="icon" :is="icon" /></el-icon>
{{title}}
</span>
<span class="sc-contextmenu__suffix">
<el-icon v-if="$slots.default"><el-icon-arrow-right /></el-icon>
<template v-else>{{suffix}}</template>
</span>
<ul v-if="$slots.default" class="sc-contextmenu__menu">
<slot></slot>
</ul>
</li>
</template>
<script>
export default {
props: {
command: { type: String, default: "" },
title: { type: String, default: "" },
suffix: { type: String, default: "" },
icon: { type: String, default: "" },
divided: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
},
inject: ['menuClick'],
methods: {
liClick(){
if(this.$slots.default){
return false
}
if(this.disabled){
return false
}
this.menuClick(this.command)
},
openSubmenu(e){
var menu = e.target.querySelector('ul')
if(!menu){
return false
}
menu.style.display = 'inline-block'
var rect = menu.getBoundingClientRect()
var menuX = rect.left
var menuY = rect.top
var innerWidth = window.innerWidth
var innerHeight = window.innerHeight
var menuHeight = menu.offsetHeight
var menuWidth = menu.offsetWidth
if(menuX + menuWidth > innerWidth){
menu.style.left = 'auto'
menu.style.right = '100%'
}
if(menuY + menuHeight > innerHeight){
menu.style.top = 'auto'
menu.style.bottom = '0'
}
},
closeSubmenu(e){
var menu = e.target.querySelector('ul')
if(!menu){
return false
}
menu.removeAttribute("style")
menu.style.display = 'none'
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,97 @@
<template>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="form.type">
<el-radio-button label="任意值" :value="0"></el-radio-button>
<el-radio-button label="范围" :value="1"></el-radio-button>
<el-radio-button label="间隔" :value="2"></el-radio-button>
<el-radio-button label="指定" :value="3"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="form.type==1">
<el-input-number v-model="form.range.start" :min="1" :max="31" controls-position="right"></el-input-number>
<span style="padding:0 15px;">-</span>
<el-input-number v-model="form.range.end" :min="1" :max="31" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="间隔" v-if="form.type==2">
<el-input-number v-model="form.loop.start" :min="1" :max="31" controls-position="right"></el-input-number>
号开始
<el-input-number v-model="form.loop.end" :min="1" :max="31" controls-position="right"></el-input-number>
天执行一次
</el-form-item>
<el-form-item label="指定" v-if="form.type==3">
<el-select v-model="form.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in day" :key="index" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-form>
</template>
<script>
export default {
emits: ['update:modelValue'],
props: {
modelValue: { type: String, default: "*" },
},
data(){
return {
form: {
type: '0',
range: {
start: 1,
end: 1
},
loop: {
start: 1,
end: 1
},
appoint: []
},
day: []
}
},
watch:{
modelValue:{
handler(val){
if(val == '*'){
this.form.type = 0
}else if(val.indexOf('-') > -1){
this.form.type = 1
}else if(val.indexOf('/') > -1){
this.form.type = 2
}else if(val.indexOf(',') > -1){
this.form.type = 3
}else{
this.form.type = 3
}
},
immediate: true
},
"form": {
handler(val){
let data = ''
if(val.type == 0){
data = '*'
}else if(val.type==1){
data = this.form.range.start + '-' + this.form.range.end
}else if(val.type==2){
data = this.form.loop.start + '/' + this.form.loop.end
}else if(val.type==3){
data = this.form.appoint.length>0 ? this.form.appoint.join(',') : '1'
}else{
data = '*'
}
this.$emit('update:modelValue', data)
},
deep: true
}
},
mounted(){
},
methods:{
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,97 @@
<template>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="form.type">
<el-radio-button label="任意值" :value="0"></el-radio-button>
<el-radio-button label="范围" :value="1"></el-radio-button>
<el-radio-button label="间隔" :value="2"></el-radio-button>
<el-radio-button label="指定" :value="3"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="form.type==1">
<el-input-number v-model="form.range.start" :min="0" :max="23" controls-position="right"></el-input-number>
<span style="padding:0 15px;">-</span>
<el-input-number v-model="form.range.end" :min="0" :max="23" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="间隔" v-if="form.type==2">
<el-input-number v-model="form.loop.start" :min="0" :max="23" controls-position="right"></el-input-number>
小时开始
<el-input-number v-model="form.loop.end" :min="0" :max="23" controls-position="right"></el-input-number>
小时执行一次
</el-form-item>
<el-form-item label="指定" v-if="form.type==3">
<el-select v-model="form.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in hour" :key="index" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-form>
</template>
<script>
export default {
emits: ['update:modelValue'],
props: {
modelValue: { type: String, default: "*" },
},
data(){
return {
form: {
type: '0',
range: {
start: 1,
end: 1
},
loop: {
start: 1,
end: 1
},
appoint: []
},
hour: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
}
},
watch:{
modelValue:{
handler(val){
if(val == '*'){
this.form.type = 0
}else if(val.indexOf('-') > -1){
this.form.type = 1
}else if(val.indexOf('/') > -1){
this.form.type = 2
}else if(val.indexOf(',') > -1){
this.form.type = 3
}else{
this.form.type = 3
}
},
immediate: true
},
"form": {
handler(val){
let data = ''
if(val.type == 0){
data = '*'
}else if(val.type==1){
data = this.form.range.start + '-' + this.form.range.end
}else if(val.type==2){
data = this.form.loop.start + '/' + this.form.loop.end
}else if(val.type==3){
data = this.form.appoint.length>0 ? this.form.appoint.join(',') : '1'
}else{
data = '*'
}
this.$emit('update:modelValue', data)
},
deep: true
}
},
mounted(){
},
methods:{
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,97 @@
<template>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="form.type">
<el-radio-button label="任意值" :value="0"></el-radio-button>
<el-radio-button label="范围" :value="1"></el-radio-button>
<el-radio-button label="间隔" :value="2"></el-radio-button>
<el-radio-button label="指定" :value="3"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="form.type==1">
<el-input-number v-model="form.range.start" :min="0" :max="23" controls-position="right"></el-input-number>
<span style="padding:0 15px;">-</span>
<el-input-number v-model="form.range.end" :min="0" :max="23" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="间隔" v-if="form.type==2">
<el-input-number v-model="form.loop.start" :min="0" :max="23" controls-position="right"></el-input-number>
分钟开始
<el-input-number v-model="form.loop.end" :min="0" :max="23" controls-position="right"></el-input-number>
分钟执行一次
</el-form-item>
<el-form-item label="指定" v-if="form.type==3">
<el-select v-model="form.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in minute" :key="index" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-form>
</template>
<script>
export default {
emits: ['update:modelValue'],
props: {
modelValue: { type: String, default: "*" },
},
data(){
return {
form: {
type: '0',
range: {
start: 1,
end: 1
},
loop: {
start: 1,
end: 1
},
appoint: []
},
minute: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,51, 52, 53, 54, 55, 56, 57, 58, 59]
}
},
watch:{
modelValue:{
handler(val){
if(val == '*'){
this.form.type = 0
}else if(val.indexOf('-') > -1){
this.form.type = 1
}else if(val.indexOf('/') > -1){
this.form.type = 2
}else if(val.indexOf(',') > -1){
this.form.type = 3
}else{
this.form.type = 3
}
},
immediate: true
},
"form": {
handler(val){
let data = ''
if(val.type == 0){
data = '*'
}else if(val.type==1){
data = this.form.range.start + '-' + this.form.range.end
}else if(val.type==2){
data = this.form.loop.start + '/' + this.form.loop.end
}else if(val.type==3){
data = this.form.appoint.length>0 ? this.form.appoint.join(',') : '1'
}else{
data = '*'
}
this.$emit('update:modelValue', data)
},
deep: true
}
},
mounted(){
},
methods:{
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,99 @@
<template>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="form.type">
<el-radio-button label="任意值" :value="0"></el-radio-button>
<el-radio-button label="范围" :value="1"></el-radio-button>
<el-radio-button label="间隔" :value="2"></el-radio-button>
<el-radio-button label="指定" :value="3"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="form.type==1">
<el-input-number v-model="form.range.start" :min="1" :max="12" controls-position="right"></el-input-number>
<span style="padding:0 15px;">-</span>
<el-input-number v-model="form.range.end" :min="1" :max="12" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="间隔" v-if="form.type==2">
<el-input-number v-model="form.loop.start" :min="1" :max="12" controls-position="right"></el-input-number>
月开始
<el-input-number v-model="form.loop.end" :min="1" :max="12" controls-position="right"></el-input-number>
月执行一次
</el-form-item>
<el-form-item label="指定" v-if="form.type==3">
<el-select v-model="form.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in data.month" :key="index" :label="item" :value="item"></el-option>
</el-select>
</el-form-item>
</el-form>
</template>
<script>
export default {
emits: ['update:modelValue'],
props: {
modelValue: { type: String, default: "*" },
},
data(){
return {
form: {
type: '0',
range: {
start: 1,
end: 1
},
loop: {
start: 1,
end: 1
},
appoint: []
},
data: {
month: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
}
}
},
watch:{
modelValue:{
handler(val){
if(val == '*'){
this.form.type = 0
}else if(val.indexOf('-') > -1){
this.form.type = 1
}else if(val.indexOf('/') > -1){
this.form.type = 2
}else if(val.indexOf(',') > -1){
this.form.type = 3
}else{
this.form.type = 3
}
},
immediate: true
},
"form": {
handler(val){
let data = ''
if(val.type == 0){
data = '*'
}else if(val.type==1){
data = this.form.range.start + '-' + this.form.range.end
}else if(val.type==2){
data = this.form.loop.start + '/' + this.form.loop.end
}else if(val.type==3){
data = this.form.appoint.length>0 ? this.form.appoint.join(',') : '1'
}else{
data = '*'
}
this.$emit('update:modelValue', data)
},
deep: true
}
},
mounted(){
},
methods:{
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,97 @@
<template>
<el-form>
<el-form-item label="类型">
<el-radio-group v-model="form.type">
<el-radio-button label="任意值" :value="0"></el-radio-button>
<el-radio-button label="范围" :value="1"></el-radio-button>
<el-radio-button label="间隔" :value="2"></el-radio-button>
<el-radio-button label="指定" :value="3"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="范围" v-if="form.type==1">
<el-input-number v-model="form.range.start" :min="0" :max="59" controls-position="right"></el-input-number>
<span style="padding:0 15px;">-</span>
<el-input-number v-model="form.range.end" :min="0" :max="59" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="间隔" v-if="form.type==2">
<el-input-number v-model="form.loop.start" :min="0" :max="59" controls-position="right"></el-input-number>
秒开始
<el-input-number v-model="form.loop.end" :min="0" :max="59" controls-position="right"></el-input-number>
秒执行一次
</el-form-item>
<el-form-item label="指定" v-if="form.type==3">
<el-select v-model="form.appoint" multiple style="width: 100%;">
<el-option v-for="(item, index) in second" :key="index" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-form>
</template>
<script>
export default {
emits: ['update:modelValue'],
props: {
modelValue: { type: String, default: "*" },
},
data(){
return {
form: {
type: '0',
range: {
start: 0,
end: 59
},
loop: {
start: 0,
end: 59
},
appoint: []
},
second: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59]
}
},
watch:{
modelValue:{
handler(val){
if(val == '*'){
this.form.type = 0
}else if(val.indexOf('-') > -1){
this.form.type = 1
}else if(val.indexOf('/') > -1){
this.form.type = 2
}else if(val.indexOf(',') > -1){
this.form.type = 3
}else{
this.form.type = 3
}
},
immediate: true
},
"form": {
handler(val){
let data = ''
if(val.type == 0){
data = '*'
}else if(val.type==1){
data = this.form.range.start + '-' + this.form.range.end
}else if(val.type==2){
data = this.form.loop.start + '/' + this.form.loop.end
}else if(val.type==3){
data = this.form.appoint.length>0 ? this.form.appoint.join(',') : '1'
}else{
data = '*'
}
this.$emit('update:modelValue', data)
},
deep: true
}
},
mounted(){
},
methods:{
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,125 @@
<template>
<el-input v-model="defaultValue" v-bind="$attrs">
<template #append>
<el-dropdown size="medium" @command="handleShortcuts">
<el-button icon="el-icon-arrow-down"></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="* * * * *">每分钟</el-dropdown-item>
<el-dropdown-item command="0 * * * *">每小时</el-dropdown-item>
<el-dropdown-item command="0 0 * * *">每天零点</el-dropdown-item>
<el-dropdown-item command="0 0 1 * *">每月一号零点</el-dropdown-item>
<el-dropdown-item command="0 0 L * *">每月最后一天零点</el-dropdown-item>
<el-dropdown-item command="0 0 ? * 1">每周星期日零点</el-dropdown-item>
<el-dropdown-item v-for="(item, index) in shortcuts" :key="item.value" :divided="index==0" :command="item.value">{{item.text}}</el-dropdown-item>
<el-dropdown-item icon="el-icon-plus" divided command="custom">自定义</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-input>
<el-dialog title="cron规则生成器" v-model="dialogVisible" :width="580" destroy-on-close append-to-body>
<div class="sc-cron">
<el-tabs v-model="activeName">
<el-tab-pane v-for="(item, index) in tabsList" :key="index" :name="item.name">
<template #label>
<div class="sc-cron-num">
<h2>{{item.label}}</h2>
<h4>{{item.value}}</h4>
</div>
</template>
<component :is="item.name" v-model="item.value" />
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<el-button @click="dialogVisible=false" > </el-button>
<el-button type="primary" @click="submit()"> </el-button>
</template>
</el-dialog>
</template>
<script>
import second from './components/second.vue'
import minute from './components/minute.vue'
import hour from './components/hour.vue'
import day from './components/day.vue'
import month from './components/month.vue'
export default {
props: {
modelValue: { type: String, default: "* * * * * ?" },
shortcuts: { type: Array, default: () => [] }
},
data(){
return {
defaultValue: '',
dialogVisible: false,
activeName: 'second',
tabsList: [
{name: 'second', label: '秒', value: '*'},
{name: 'minute', label: '分钟', value: '*'},
{name: 'hour', label: '小时', value: '*'},
{name: 'day', label: '日', value: '*'},
{name: 'month', label: '月', value: '*'},
// {name: 'week', label: '周', value: '*'}
// {name: 'year', label: '年', value: '*'}
]
}
},
watch:{
modelValue: {
handler(val){
this.defaultValue = val
let arr = val.split(' ')
this.tabsList.forEach((item, index) => {
item.value = arr[index]
})
},
immediate: true,
deep: true
}
},
components:{
second,
minute,
hour,
day,
month
},
mounted(){
},
methods:{
handleShortcuts(command){
if(command == 'custom'){
this.open()
}else{
this.defaultValue = command
this.$emit('update:modelValue', this.defaultValue)
}
},
open(){
this.dialogVisible = true
},
submit(){
let data = []
for(let item of this.tabsList) {
data.push(item.value)
}
this.defaultValue = data.join(' ')
this.dialogVisible = false
this.$emit('update:modelValue', this.defaultValue)
}
}
}
</script>
<style scoped>
.sc-cron:deep(.el-tabs__item) {height: auto;line-height: 1;padding:0 7px;vertical-align: bottom;}
.sc-cron-num {text-align: center;margin-bottom: 15px;width: 100%;}
.sc-cron-num h2 {font-size: 12px;margin-bottom: 15px;font-weight: normal;}
.sc-cron-num h4 {display: block;height: 32px;line-height: 30px;width: 100%;font-size: 12px;padding:0 15px;background: var(--el-color-primary-light-9);border-radius:4px;}
.sc-cron:deep(.el-tabs__item.is-active) .sc-cron-num h4 {background: var(--el-color-primary);color: #fff;}
[data-theme='dark'] .sc-cron-num h4 {background: var(--el-color-white);}
</style>
@@ -0,0 +1,84 @@
<!--
* @Descripttion: 图像裁剪组件
* @version: 1.0
* @Author: sakuya
* @Date: 2021年7月24日17:05:43
* @LastEditors:
* @LastEditTime:
* @other: 代码完全开源欢迎参考也欢迎PR
-->
<template>
<div class="sc-cropper">
<div class="sc-cropper__img">
<img :src="src" ref="img">
</div>
<div class="sc-cropper__preview">
<h4>图像预览</h4>
<div class="sc-cropper__preview__img" ref="preview"></div>
</div>
</div>
</template>
<script>
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
export default {
props: {
src: { type: String, default: "" },
compress: {type: Number, default: 1},
aspectRatio: {type: Number, default: NaN},
},
data() {
return {
crop: null
}
},
watch:{
aspectRatio(val){
this.crop.setAspectRatio(val)
}
},
mounted() {
this.init()
},
methods: {
init(){
this.crop = new Cropper(this.$refs.img, {
viewMode: 2,
dragMode: 'move',
responsive: false,
aspectRatio: this.aspectRatio,
preview: this.$refs.preview
})
},
setAspectRatio(aspectRatio){
this.crop.setAspectRatio(aspectRatio)
},
getCropData(cb, type='image/jpeg'){
cb(this.crop.getCroppedCanvas().toDataURL(type, this.compress))
},
getCropBlob(cb, type='image/jpeg'){
this.crop.getCroppedCanvas().toBlob((blob) => {
cb(blob)
}, type, this.compress)
},
getCropFile(cb, fileName='fileName.jpg', type='image/jpeg'){
this.crop.getCroppedCanvas().toBlob((blob) => {
let file = new File([blob], fileName, {type: type})
cb(file)
}, type, this.compress)
}
}
}
</script>
<style scoped>
.sc-cropper {height:300px;}
.sc-cropper__img {height:100%;width:400px;float: left;background: #EBEEF5;}
.sc-cropper__img img {display: none;}
.sc-cropper__preview {width: 120px;margin-left: 20px;float: left;}
.sc-cropper__preview h4 {font-weight: normal;font-size: 12px;color: #999;margin-bottom: 20px;}
.sc-cropper__preview__img {overflow: hidden;width: 120px;height: 120px;border: 1px solid #ebeef5;}
</style>
@@ -0,0 +1,84 @@
<!--
* @Descripttion: 弹窗扩展组件
* @version: 2.0
* @Author: sakuya
* @Date: 2021年8月27日08:51:52
* @LastEditors: sakuya
* @LastEditTime: 2022年5月14日15:13:41
-->
<template>
<div class="sc-dialog" ref="scDialog">
<el-dialog ref="dialog" v-model="dialogVisible" :fullscreen="isFullscreen" v-bind="$attrs" :show-close="false">
<template #header>
<slot name="header">
<span class="el-dialog__title">{{ title }}</span>
</slot>
<div class="sc-dialog__headerbtn">
<button v-if="showFullscreen" aria-label="fullscreen" type="button" @click="setFullscreen">
<el-icon v-if="isFullscreen" class="el-dialog__close"><el-icon-bottom-left /></el-icon>
<el-icon v-else class="el-dialog__close"><el-icon-full-screen /></el-icon>
</button>
<button v-if="showClose" aria-label="close" type="button" @click="closeDialog">
<el-icon class="el-dialog__close"><el-icon-close /></el-icon>
</button>
</div>
</template>
<div v-loading="loading">
<slot></slot>
</div>
<template #footer>
<slot name="footer"></slot>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
props: {
modelValue: { type: Boolean, default: false },
title: { type: String, default: "" },
showClose: { type: Boolean, default: true },
showFullscreen: { type: Boolean, default: true },
loading: { type: Boolean, default: false }
},
data() {
return {
dialogVisible: false,
isFullscreen: false
}
},
watch:{
modelValue(){
this.dialogVisible = this.modelValue
if(this.dialogVisible){
this.isFullscreen = false
}
}
},
mounted() {
this.dialogVisible = this.modelValue
},
methods: {
//关闭
closeDialog(){
this.dialogVisible = false
},
//最大化
setFullscreen(){
this.isFullscreen = !this.isFullscreen
}
}
}
</script>
<style scoped>
.sc-dialog__headerbtn {position: absolute;top: var(--el-dialog-padding-primary);right: var(--el-dialog-padding-primary);}
.sc-dialog__headerbtn button {padding: 0;background: transparent;border: none;outline: none;cursor: pointer;font-size: var(--el-message-close-size,16px);margin-left: 15px;color: var(--el-color-info);}
.sc-dialog__headerbtn button:hover .el-dialog__close {color: var(--el-color-primary);}
.sc-dialog:deep(.el-dialog).is-fullscreen {display: flex;flex-direction: column;top:0px !important;left:0px !important;}
.sc-dialog:deep(.el-dialog).is-fullscreen .el-dialog__header {}
.sc-dialog:deep(.el-dialog).is-fullscreen .el-dialog__body {flex:1;overflow: auto;}
.sc-dialog:deep(.el-dialog).is-fullscreen .el-dialog__footer {padding-bottom: 10px;border-top: 1px solid var(--el-border-color-base);}
</style>
@@ -0,0 +1,74 @@
const T = {
"color": [
"#409EFF",
"#36CE9E",
"#f56e6a",
"#626c91",
"#edb00d",
"#909399"
],
'grid': {
'left': '3%',
'right': '3%',
'bottom': '10',
'top': '40',
'containLabel': true
},
"legend": {
"textStyle": {
"color": "#999"
},
"inactiveColor": "rgba(128,128,128,0.4)"
},
"categoryAxis": {
"axisLine": {
"show": true,
"lineStyle": {
"color": "rgba(128,128,128,0.2)",
"width": 1
}
},
"axisTick": {
"show": false,
"lineStyle": {
"color": "#333"
}
},
"axisLabel": {
"color": "#999"
},
"splitLine": {
"show": false,
"lineStyle": {
"color": [
"#eee"
]
}
},
"splitArea": {
"show": false,
"areaStyle": {
"color": [
"rgba(255,255,255,0.01)",
"rgba(0,0,0,0.01)"
]
}
}
},
"valueAxis": {
"axisLine": {
"show": false,
"lineStyle": {
"color": "#999"
}
},
"splitLine": {
"show": true,
"lineStyle": {
"color": "rgba(128,128,128,0.2)"
}
}
}
}
export default T
@@ -0,0 +1,64 @@
<template>
<div ref="scEcharts" :style="{height:height, width:width}"></div>
</template>
<script>
import * as echarts from 'echarts';
import T from './echarts-theme-T.js';
echarts.registerTheme('T', T);
const unwarp = (obj) => obj && (obj.__v_raw || obj.valueOf() || obj);
export default {
...echarts,
name: "scEcharts",
props: {
height: { type: String, default: "100%" },
width: { type: String, default: "100%" },
nodata: {type: Boolean, default: false },
option: { type: Object, default: () => {} }
},
data() {
return {
isActivat: false,
myChart: null
}
},
watch: {
option: {
deep:true,
handler (v) {
unwarp(this.myChart).setOption(v);
}
}
},
computed: {
myOptions: function() {
return this.option || {};
}
},
activated(){
if(!this.isActivat){
this.$nextTick(() => {
this.myChart.resize()
})
}
},
deactivated(){
this.isActivat = false;
},
mounted(){
this.isActivat = true;
this.$nextTick(() => {
this.draw();
})
},
methods: {
draw(){
var myChart = echarts.init(this.$refs.scEcharts, 'T');
myChart.setOption(this.myOptions);
this.myChart = myChart;
window.addEventListener('resize', () => myChart.resize());
}
}
}
</script>
@@ -0,0 +1,80 @@
export default class UploadAdapter {
constructor( loader, options ) {
this.loader = loader;
this.options = options;
}
upload() {
return this.loader.file
.then( file => new Promise( ( resolve, reject ) => {
this._initRequest();
this._initListeners( resolve, reject, file );
this._sendRequest( file );
} ) );
}
abort() {
if ( this.xhr ) {
this.xhr.abort();
}
}
_initRequest() {
const xhr = this.xhr = new XMLHttpRequest();
xhr.open( 'POST', this.options.upload.uploadUrl, true );
xhr.responseType = 'json';
}
_initListeners( resolve, reject, file ) {
const xhr = this.xhr;
const loader = this.loader;
const genericErrorText = `Couldn't upload file: ${ file.name }.`;
xhr.addEventListener( 'error', () => reject( genericErrorText ) );
xhr.addEventListener( 'abort', () => reject() );
xhr.addEventListener( 'load', () => {
const response = xhr.response;
if ( !response || response.code == 0 ) {
return reject( response && response.code == 0 ? response.message : genericErrorText );
}
resolve( {
default: response.data.url ? response.data.url : response.data.src
} );
} );
if ( xhr.upload ) {
xhr.upload.addEventListener( 'progress', evt => {
if ( evt.lengthComputable ) {
loader.uploadTotal = evt.total;
loader.uploaded = evt.loaded;
}
} );
}
}
_sendRequest( file ) {
// Set headers if specified.
const headers = this.options.upload.headers || {};
const extendData = this.options.upload.extendData || {};
// Use the withCredentials flag if specified.
const withCredentials = this.options.upload.withCredentials || false;
const uploadName = this.options.upload.uploadName || 'file';
for (const headerName of Object.keys(headers)) {
this.xhr.setRequestHeader(headerName, headers[headerName]);
}
this.xhr.withCredentials = withCredentials;
const data = new FormData();
for (const key of Object.keys(extendData)) {
data.append(key, extendData[key]);
}
data.append( uploadName, file );
this.xhr.send( data );
}
}
export function UploadAdapterPlugin( editor ) {
editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => {
return new UploadAdapter( loader, editor.config._config );
};
}
@@ -0,0 +1,180 @@
<template>
<div :style="{'--editor-height': editorHeight}">
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig" :disabled="disabled" @blur="onBlur" @focus="onFocus"></ckeditor>
</div>
</template>
<script>
import { ClassicEditor, Alignment, AutoImage, Autoformat, BlockQuote, Bold, CodeBlock, DataFilter, DataSchema, Essentials, FindAndReplace,
FontBackgroundColor, FontColor, FontFamily, FontSize, GeneralHtmlSupport, Heading, Highlight, HorizontalLine, Image, ImageCaption, ImageInsert, ImageResize,
ImageStyle, ImageToolbar, ImageUpload, Indent, IndentBlock, Italic, Link, LinkImage, List, MediaEmbed, MediaEmbedToolbar, Mention, Paragraph, PasteFromOffice,
RemoveFormat, SelectAll, ShowBlocks, SourceEditing, SpecialCharacters, SpecialCharactersArrows, SpecialCharactersCurrency, SpecialCharactersEssentials, SpecialCharactersLatin,
SpecialCharactersMathematical, SpecialCharactersText, Style, Subscript, Superscript, Table, TableCaption, TableCellProperties, TableColumnResize,
TableProperties, TableToolbar, TextTransformation, TodoList, Underline, Undo, WordCount
} from 'ckeditor5';
import { Ckeditor } from '@ckeditor/ckeditor5-vue';
import { UploadAdapterPlugin } from './UploadAdapter.js'
import coreTranslations from 'ckeditor5/translations/zh-cn.js'
import 'ckeditor5/ckeditor5.css';
export default {
name: 'scCkeditor',
props: {
modelValue: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '请输入内容……'
},
toolbar: {
type: String,
default: 'basic'
},
height: {
type: String,
default: '400px'
},
disabled: {
type: Boolean,
default: false
}
},
components: {
Ckeditor
},
data(){
return {
editorData: '',
editor: ClassicEditor,
editorHeight: 0,
toolbars: {
full: [
'sourceEditing', 'undo', 'redo', 'heading', 'style',
'|', 'superscript', 'subscript', 'removeFormat', 'bold', 'italic', 'underline', 'link', 'fontBackgroundColor', 'fontFamily', 'fontSize', 'fontColor',
'|', 'outdent', 'indent', 'alignment', 'bulletedList', 'numberedList', 'todoList',
'|', 'blockQuote', 'insertTable', 'imageInsert', 'mediaEmbed', 'highlight', 'horizontalLine', 'selectAll', 'showBlocks', 'specialCharacters', 'codeBlock', 'findAndReplace'
],
basic: [
'sourceEditing', 'undo', 'redo', 'heading',
'|', 'removeFormat', 'bold', 'italic', 'underline', 'link', 'fontBackgroundColor', 'fontFamily', 'fontSize', 'fontColor',
'|', 'outdent', 'indent', 'alignment', 'bulletedList', 'numberedList', 'todoList',
'|', 'insertTable', 'imageInsert', 'mediaEmbed'
],
simple: [
'undo', 'redo', 'heading',
'|', 'removeFormat', 'bold', 'italic', 'underline', 'link', 'fontBackgroundColor', 'fontFamily', 'fontSize', 'fontColor',
'|', 'insertTable', 'imageInsert', 'mediaEmbed'
]
},
editorConfig: {
language: {ui: 'zh-cn', content: 'zh-cn'},
translations: [ coreTranslations ],
plugins: [ Alignment, AutoImage, Autoformat, BlockQuote, Bold, CodeBlock, DataFilter, DataSchema, Essentials, FindAndReplace,
FontBackgroundColor, FontColor, FontFamily, FontSize, GeneralHtmlSupport, Heading, Highlight, HorizontalLine, Image, ImageCaption, ImageInsert, ImageResize,
ImageStyle, ImageToolbar, ImageUpload, Indent, IndentBlock, Italic, Link, LinkImage, List, MediaEmbed, MediaEmbedToolbar, Mention, Paragraph, PasteFromOffice,
RemoveFormat, SelectAll, ShowBlocks, SourceEditing, SpecialCharacters, SpecialCharactersArrows, SpecialCharactersCurrency, SpecialCharactersEssentials, SpecialCharactersLatin,
SpecialCharactersMathematical, SpecialCharactersText, Style, Subscript, Superscript, Table, TableCaption, TableCellProperties, TableColumnResize,
TableProperties, TableToolbar, TextTransformation, TodoList, Underline, Undo, WordCount, UploadAdapterPlugin
],
toolbar: {shouldNotGroupWhenFull: true},
placeholder: '',
image: {
styles: ['alignLeft', 'alignCenter', 'alignRight'],
toolbar: ['imageTextAlternative', 'toggleImageCaption', '|', 'imageStyle:alignLeft', 'imageStyle:alignCenter', 'imageStyle:alignRight', '|', 'linkImage']
},
mediaEmbed: {
providers: [
{name: 'mp4', url: /\.(mp4|avi|mov|flv|wmv|mkv)$/i, html: match => {
const url = match['input']
return ('<video controls width="100%" height="100%" src="${url}"></video>')
}
}
]
},
fontSize: {
options: [
10, 12, 14, 16, 18, 20, 22, 24, 26, 30, 32, 36
]
},
style: {
definitions: [
{
name: 'Article category',
element: 'h3',
classes: [ 'category' ]
},
{
name: 'Info box',
element: 'p',
classes: [ 'info-box' ]
},
]
},
upload: {
uploadUrl: this.$API.common.upload.url,
withCredentials: false,
extendData: {type: 'images'},
headers: {
Authorization: 'Bearer ' + this.$TOOL.data.get('TOKEN')
}
},
}
}
},
watch: {
modelValue: {
handler(newVal) {
this.editorData = newVal ?? ''
},
immediate: true
},
placeholder: {
handler(newVal) {
this.editorConfig.placeholder = newVal
},
immediate: true,
deep: true
},
toolbar: {
handler(newVal) {
this.editorConfig.toolbar.items = this.toolbars[newVal]
},
immediate: true,
deep: true
},
height: {
handler(newVal) {
this.editorHeight = newVal
},
immediate: true,
deep: true
}
},
mounted() {
},
methods: {
onBlur(event, editor) {
this.editorData = this.editorData.replace(/<img[^>]*>/gi, (match) => {
return match.replace(/width="[^"]*"/gi, '').replace(/height="[^"]*"/gi, '')
})
this.$emit('update:modelValue', this.editorData)
},
onFocus(event, editor) {
}
}
}
</script>
<style>
:root{
--ck-z-panel: 9999
}
.ck-content{
height: var(--editor-height);
}
.ck-source-editing-area, .ck-source-editing-area textarea{height: var(--editor-height);}
.ck-source-editing-area textarea{overflow-y: scroll !important;}
</style>
@@ -0,0 +1,210 @@
<template>
<div ref="aiEditor" :style="{height: editorHeight, width: '100%'}"></div>
</template>
<script>
import {AiEditor} from "aieditor";
import "aieditor/dist/style.css"
export default {
name: 'scCkeditor',
emits: ['update:modelValue'],
props: {
modelValue: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '请输入内容……'
},
toolbar: {
type: String,
default: 'basic'
},
height: {
type: String,
default: '400px'
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
editor: null,
defaultValue: '',
editorHeight: '300px',
toolbars: {
full: [
"undo", "redo", "brush", "eraser",
"|", "heading", "font-family", "font-size",
"|", "bold", "italic", "underline", "strike", "link", "code", "subscript", "superscript", "hr", "todo", "emoji",
"|", "highlight", "font-color",
"|", "align", "line-height",
"|", "bullet-list", "ordered-list", "indent-decrease", "indent-increase", "break",
"|", "image", "video", "attachment", "quote", "code-block", "table",
"|", "source-code", "printer", "fullscreen", "ai"
],
basic: [
"undo", "redo", "brush", "eraser",
"|", "heading", "font-family", "font-size",
"|", "bold", "italic", "underline", "strike", "link", "code", "subscript", "superscript", "hr", "todo", "emoji",
"|", "highlight", "font-color",
"|", "align", "line-height",
"|", "bullet-list", "ordered-list", "indent-decrease", "indent-increase", "break",
"|", "image", "video", "attachment", "quote", "code-block", "table",
],
simple: ["undo", "redo",
"|", "font-family", "font-size",
"|", "bold", "italic", "underline", "strike", "emoji",
"|", "highlight", "font-color",
"|", "image", "link"]
}
}
},
watch: {
modelValue(newValue) {
if (newValue !== this.defaultValue) {
this.defaultValue = newValue
}
},
height: {
handler(newValue) {
if (indexOf(newValue, 'px') === -1){
this.editorHeight = newValue > 200 ? newValue + 'px' : '200px'
}else{
newValue = newValue.replace('px', '')
this.editorHeight = newValue > 200 ? newValue + 'px' : '200px'
}
},
immediate: true
}
},
mounted() {
this.editor = new AiEditor({
element: this.$refs.aiEditor,
placeholder: this.placeholder,
content: this.defaultValue,
toolbarKeys: this.toolbars[this.toolbar],
onChange: (editor) => {
this.$emit('update:modelValue', editor.getHtml())
},
editable: !this.disabled,
textSelectionBubbleMenu: {
enable: true,
items: ["Bold", "Italic", "Underline", "Strike", "code", "comment"],
},
image: {
uploadUrl: this.$API.common.upload.url,
uploadHeaders: {
Authorization: 'Bearer ' + this.$TOOL.data.get('TOKEN')
},
uploadFormName: 'file',
uploader: (file, uploadUrl, headers, formName) => {
const formData = new FormData();
formData.append(formName, file);
formData.append('type', 'images');
return new Promise((resolve, reject) => {
fetch(uploadUrl, {
method: "post",
headers: {'Accept': 'application/json', ...headers},
body: formData,
}).then((resp) => resp.json())
.then(json => {
resolve({json});
}).catch((error) => {
reject(error);
})
});
},
uploaderEvent: {
onSuccess: (file, response) => {
let res = response.json
if (res.code === 1) {
return {errorCode: 0, data: {
src: res.data.url
}}
} else {
return false
}
}
}
},
video: {
uploadUrl: this.$API.common.upload.url,
uploadHeaders: {
Authorization: 'Bearer ' + this.$TOOL.data.get('TOKEN')
},
uploadFormName: 'file',
uploader: (file, uploadUrl, headers, formName) => {
const formData = new FormData();
formData.append(formName, file);
formData.append('type', 'video');
return new Promise((resolve, reject) => {
fetch(uploadUrl, {
method: "post",
headers: {'Accept': 'application/json', ...headers},
body: formData,
}).then((resp) => resp.json())
.then(json => {
resolve(json);
}).catch((error) => {
reject(error);
})
});
},
uploaderEvent: {
onSuccess: (file, response) => {
let res = response.json
if (res.code === 1) {
return {errorCode: 0, data: {
src: res.data.url
}}
} else {
return false
}
}
}
},
attachment: {
uploadUrl: this.$API.common.upload.url,
uploadHeaders: {
Authorization: 'Bearer ' + this.$TOOL.data.get('TOKEN')
},
uploadFormName: 'file',
uploader: (file, uploadUrl, headers, formName) => {
const formData = new FormData();
formData.append(formName, file);
formData.append('type', 'files');
return new Promise((resolve, reject) => {
fetch(uploadUrl, {
method: "post",
headers: {'Accept': 'application/json', ...headers},
body: formData,
}).then((resp) => resp.json())
.then(json => {
resolve(json);
}).catch((error) => {
reject(error);
})
});
},
uploaderEvent: {
onSuccess: (file, response) => {
let res = response.json
if (res.code === 1) {
return {errorCode: 0, data: {
src: res.data.url
}}
} else {
return false
}
}
}
},
})
}
}
</script>
@@ -0,0 +1,53 @@
<template>
<el-table ref="table" :data="columnData" row-key="prop" style="width: 100%" border>
<el-table-column prop="" label="排序" width="60">
<el-tag class="move" style="cursor: move;"><el-icon style="cursor: move;"><el-icon-d-caret/></el-icon></el-tag>
</el-table-column>
<el-table-column prop="label" label="列名">
<template #default="scope">
<el-tag round :effect="scope.row.hide?'light':'dark'" :type="scope.row.hide?'info':''">{{ scope.row.label }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="hide" label="显示" width="60">
<template #default="scope">
<el-switch v-model="scope.row.hide" :active-value="false" :inactive-value="true"/>
</template>
</el-table-column>
</el-table>
</template>
<script>
import Sortable from 'sortablejs'
export default {
emits: ['success'],
props: {
column: { type: Array, default: () => [] }
},
data() {
return {
columnData: this.column
}
},
mounted() {
this.rowDrop()
},
methods: {
rowDrop(){
const _this = this
const tbody = this.$refs.table.$el.querySelector('.el-table__body-wrapper tbody')
Sortable.create(tbody, {
handle: ".move",
animation: 200,
ghostClass: "ghost",
onEnd({ newIndex, oldIndex }) {
const tableData = _this.columnData
const currRow = tableData.splice(oldIndex, 1)[0]
tableData.splice(newIndex, 0, currRow)
}
})
}
}
}
</script>
@@ -0,0 +1,199 @@
<!--
* @Descripttion: 文件导出
* @version: 1.1
* @Author: sakuya
* @Date: 2022年5月24日16:20:12
* @LastEditors: sakuya
* @LastEditTime: 2022年6月13日17:32:05
-->
<template>
<slot :open="open">
<el-button type="primary" plain @click="open">导出</el-button>
</slot>
<el-drawer v-model="dialog" title="导出" :size="400" direction="rtl" append-to-body destroy-on-close>
<el-main style="padding: 0 20px 20px 20px;">
<div v-loading="downLoading" element-loading-text="正在处理中...">
<div v-if="downLoading && progress" style="position: absolute;width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;z-index: 3000;">
<el-progress :text-inside="true" :stroke-width="20" :percentage="downLoadProgress" style="width: 100%;margin-bottom: 120px;"/>
</div>
<el-tabs>
<el-tab-pane label="常规" lazy>
<el-form label-width="100px" label-position="left" style="margin: 10px 0 20px 0;">
<el-form-item label="文件名">
<el-input v-model="formData.fileName" placeholder="请输入文件名" />
</el-form-item>
<el-form-item label="文件类型">
<el-select v-model="formData.fileType" placeholder="请选择文件类型">
<el-option v-for="item in fileTypes" :key="item" :label="'*.'+item" :value="item" />
</el-select>
</el-form-item>
<slot name="form" :formData="formData"></slot>
</el-form>
<el-button v-if="async" type="primary" icon="el-icon-plus" style="width: 100%;" @click="download" :loading="asyncLoading">发起导出任务</el-button>
<el-button v-else type="primary" icon="el-icon-download" style="width: 100%;" @click="download"> </el-button>
</el-tab-pane>
<el-tab-pane label="列设置" v-if="columnData.length>0" lazy>
<columnSet :column="columnData"></columnSet>
</el-tab-pane>
<el-tab-pane label="其他参数" v-if="data && showData" lazy>
<el-descriptions :column="1" border>
<el-descriptions-item v-for=" (val, key) in data" :key="key" :label="key">{{val}}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
</div>
</el-main>
</el-drawer>
</template>
<script>
import columnSet from './column'
export default {
components: {
columnSet
},
props: {
apiObj: { type: Object, default: () => {} },
fileName: { type: String, default: "" },
fileTypes: { type: Array, default: () => ['xlsx'] },
data: { type: Object, default: () => {} },
showData: { type: Boolean, default: false },
async: { type: Boolean, default: false },
column: { type: Array, default: () => [] },
blob: { type: Boolean, default: false },
progress: { type: Boolean, default: true }
},
data() {
return {
dialog: false,
formData: {
fileName: this.fileName,
fileType: this.fileTypes[0]
},
columnData: [],
downLoading: false,
downLoadProgress: 0,
asyncLoading: false
}
},
watch:{
'formData.fileType'(val) {
if(this.formData.fileName.includes(".")){
this.formData.fileName = this.formData.fileName.substring(0, this.formData.fileName.lastIndexOf('.')) + "." + val
}else{
this.formData.fileName = this.formData.fileName + "." + val
}
}
},
mounted() {
},
methods: {
open() {
this.dialog = true
this.formData = {
fileName: (this.fileName?this.fileName:(new Date().getTime()+'')) + "." + this.fileTypes[0],
fileType: this.fileTypes[0]
}
this.columnData = JSON.parse(JSON.stringify(this.column))
},
close() {
this.dialog = false
},
download() {
let columnArr = {
column: this.columnData.filter(n => !n.hide).map(n => n.prop).join(",")
}
let assignData = {...this.data, ...this.formData, ...columnArr}
if(this.async){
this.asyncDownload(this.apiObj, this.formData.fileName, assignData)
}else if(this.blob){
this.downloadFile(this.apiObj, this.formData.fileName, assignData)
}else{
this.linkFile(this.apiObj.url, this.formData.fileName, assignData)
}
},
linkFile(url, fileName, data={}){
let a = document.createElement("a")
a.style = "display: none"
a.target = "_blank"
//a.download = fileName
a.href = url + this.toQueryString(data)
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
},
downloadFile(apiObj, fileName, data={}){
this.downLoading = true
var _this = this
apiObj.get(data, {
responseType: 'blob',
onDownloadProgress(e){
if(e.lengthComputable){
_this.downLoadProgress = parseInt(e.loaded / e.total * 100)
}
}
}).then(res => {
this.downLoading = false
this.downLoadProgress = 0
// let url = URL.createObjectURL(res.data)
let url = res.data
let a = document.createElement("a")
a.style = "display: none"
a.target = "_blank"
a.download = fileName
a.href = url
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
// URL.revokeObjectURL(url)
}).catch(err => {
this.downLoading = false
this.downLoadProgress = 0
this.$notify.error({
title: '下载文件失败',
message: err
})
})
},
asyncDownload(apiObj, fileName, data={}){
this.asyncLoading = true
apiObj.get(data).then(res => {
this.asyncLoading = false
if(res.code == 1){
this.dialog = false
this.$msgbox({
title: "成功发起任务",
message: `<div><img style="height:200px" :src="'static/images/tasks-example.png'"/></div><p>已成功发起导出任务,您可以操作其他事务</p><p>稍后可在 <b>任务中心</b> 查看执行结果</p>`,
type: "success",
confirmButtonText: "知道了",
dangerouslyUseHTMLString: true,
center: true
}).catch(() => {})
}else{
this.$alert(res.message || "未知错误", "发起任务失败", {
type: "error",
center: true
}).catch(() => {})
}
}).catch(() => {
this.asyncLoading = false
})
},
toQueryString(obj){
let arr = []
for (var k in obj) {
arr.push(`${k}=${obj[k]}`)
}
return (arr.length>0?"?":"") + arr.join('&')
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,133 @@
<!--
* @Descripttion: 文件导入
* @version: 1.0
* @Author: sakuya
* @Date: 2022年5月24日11:30:03
* @LastEditors:
* @LastEditTime:
-->
<template>
<slot :open="open">
<el-button type="primary" plain @click="open">导入</el-button>
</slot>
<el-dialog v-model="dialog" title="导入" :width="550" :close-on-click-modal="false" append-to-body destroy-on-close>
<el-progress v-if="loading" :text-inside="true" :stroke-width="20" :percentage="percentage" style="margin-bottom: 15px;"/>
<div v-loading="loading">
<el-upload ref="uploader"
drag
:accept="accept"
:maxSize="maxSize"
:limit="1"
:data="data"
:show-file-list="false"
:http-request="request"
:before-upload="before"
:on-progress="progress"
:on-success="success"
:on-error="error"
>
<slot name="uploader">
<el-icon class="el-icon--upload"><el-icon-upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处或 <em>点击选择文件上传</em>
</div>
</slot>
<template #tip>
<div class="el-upload__tip">
<template v-if="tip">{{tip}}</template>
<template v-else>请上传小于或等于 {{maxSize}}M {{accept}} 格式文件</template>
<p v-if="templateUrl" style="margin-top: 7px;">
<el-link :href="templateUrl" target="_blank" type="primary" :underline="false">下载导入模板</el-link>
</p>
</div>
</template>
</el-upload>
<el-form v-if="$slots.form" inline label-width="100px" label-position="left" style="margin-top: 18px;">
<slot name="form" :formData="formData"></slot>
</el-form>
</div>
</el-dialog>
</template>
<script>
export default {
emits: ['success'],
props: {
apiObj: { type: Object, default: () => {} },
data: { type: Object, default: () => {} },
accept: { type: String, default: ".xls, .xlsx" },
maxSize: { type: Number, default: 10 },
tip: { type: String, default: "" },
templateUrl: { type: String, default: "" }
},
data() {
return {
dialog: false,
loading: false,
percentage: 0,
formData: {}
}
},
mounted() {
},
methods: {
open(){
this.dialog = true
this.formData = {}
},
close(){
this.dialog = false
},
before(file){
const maxSize = file.size / 1024 / 1024 < this.maxSize;
if (!maxSize) {
this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
return false;
}
this.loading = true
},
progress(e){
this.percentage = e.percent
},
success(res, file){
this.$refs.uploader.handleRemove(file)
this.$refs.uploader.clearFiles()
this.loading = false
this.percentage = 0
this.$emit('success', res, this.close)
},
error(err){
this.loading = false
this.percentage = 0
this.$notify.error({
title: '上传文件未成功',
message: err
})
},
request(param){
Object.assign(param.data, this.formData)
const data = new FormData();
data.append(param.filename, param.file);
for (const key in param.data) {
data.append(key, param.data[key]);
}
this.apiObj.post(data, {
onUploadProgress: e => {
const complete = parseInt(((e.loaded / e.total) * 100) | 0, 10)
param.onProgress({percent: complete})
}
}).then(res => {
param.onSuccess(res)
}).catch(err => {
param.onError(err)
})
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,283 @@
<!--
* @Descripttion: 资源文件选择器
* @version: 1.0
* @Author: sakuya
* @Date: 2021年10月11日16:01:40
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-file-select">
<div class="sc-file-select__side" v-loading="menuLoading">
<div class="sc-file-select__side-menu">
<el-tree ref="group" class="menu" :data="menu" :node-key="treeProps.key" :props="treeProps" :current-node-key="menu.length>0?menu[0][treeProps.key]:''" highlight-current @node-click="groupClick">
<template #default="{ node }">
<span class="el-tree-node__label">
<el-icon class="icon"><el-icon-folder /></el-icon>{{node.label}}
</span>
</template>
</el-tree>
</div>
<div class="sc-file-select__side-msg" v-if="multiple">
已选择 <b>{{value.length}}</b> / <b>{{max}}</b>
</div>
</div>
<div class="sc-file-select__files" v-loading="listLoading">
<div class="sc-file-select__top">
<div class="upload" v-if="!hideUpload">
<el-upload class="sc-file-select__upload" action="" multiple :show-file-list="false" :accept="accept" :on-change="uploadChange" :before-upload="uploadBefore" :on-progress="uploadProcess" :on-success="uploadSuccess" :on-error="uploadError" :http-request="uploadRequest">
<el-button type="primary" icon="el-icon-upload">本地上传</el-button>
</el-upload>
<span class="tips"><el-icon><el-icon-warning /></el-icon>大小不超过{{maxSize}}MB</span>
</div>
<div class="keyword">
<el-input v-model="keyword" prefix-icon="el-icon-search" placeholder="文件名搜索" clearable @keyup.enter="search" @clear="search"></el-input>
</div>
</div>
<div class="sc-file-select__list">
<el-scrollbar ref="scrollbar">
<el-empty v-if="fileList.length==0 && data.length==0" description="无数据" :image-size="80"></el-empty>
<div v-for="(file, index) in fileList" :key="index" class="sc-file-select__item">
<div class="sc-file-select__item__file">
<div class="sc-file-select__item__upload">
<el-progress type="circle" :percentage="file.progress" :width="70"></el-progress>
</div>
<el-image :src="file.tempImg" fit="contain"></el-image>
</div>
<p>{{file.name}}</p>
</div>
<div v-for="item in data" :key="item[fileProps.key]" class="sc-file-select__item" :class="{active: value.includes(item[fileProps.url]) }" @click="select(item)">
<div class="sc-file-select__item__file">
<div class="sc-file-select__item__checkbox" v-if="multiple">
<el-icon><el-icon-check /></el-icon>
</div>
<div class="sc-file-select__item__select" v-else>
<el-icon><el-icon-check /></el-icon>
</div>
<div class="sc-file-select__item__box"></div>
<el-image v-if="_isImg(item[fileProps.url])" :src="item[fileProps.url]" fit="contain" lazy></el-image>
<div v-else class="item-file item-file-doc">
<i v-if="files[_getExt(item[fileProps.url])]" :class="files[_getExt(item[fileProps.url])].icon" :style="{color:files[_getExt(item[fileProps.url])].color}"></i>
<i v-else class="sc-icon-file-list-fill" style="color: #999;"></i>
</div>
</div>
<p :title="item[fileProps.fileName]">{{item[fileProps.fileName]}}</p>
</div>
</el-scrollbar>
</div>
<div class="sc-file-select__pagination">
<el-pagination small background layout="prev, pager, next" :total="total" :page-size="pageSize" v-model:currentPage="currentPage" @current-change="reload"></el-pagination>
</div>
<div class="sc-file-select__do">
<slot name="do"></slot>
<el-button type="primary" :disabled="value.length<=0" @click="submit"> </el-button>
</div>
</div>
</div>
</template>
<script>
import config from "@/config/fileSelect"
export default {
props: {
modelValue: null,
hideUpload: { type: Boolean, default: false },
multiple: { type: Boolean, default: false },
max: {type: Number, default: config.max},
onlyImage: { type: Boolean, default: false },
maxSize: {type: Number, default: config.maxSize},
},
data() {
return {
keyword: null,
pageSize: 20,
total: 0,
currentPage: 1,
data: [],
menu: [],
menuId: '',
value: this.multiple ? [] : '',
fileList: [],
accept: this.onlyImage ? "image/gif, image/jpeg, image/png" : "",
listLoading: false,
menuLoading: false,
treeProps: config.menuProps,
fileProps: config.fileProps,
files: config.files
}
},
watch: {
multiple(){
this.value = this.multiple ? [] : ''
this.$emit('update:modelValue', JSON.parse(JSON.stringify(this.value)));
}
},
mounted() {
this.getMenu()
this.getData()
},
methods: {
//获取分类数据
async getMenu(){
this.menuLoading = true
var res = await config.menuApiObj.get()
this.menu = res.data
this.menuLoading = false
},
//获取列表数据
async getData(){
this.listLoading = true
var reqData = {
[config.request.menuKey]: this.menuId,
[config.request.page]: this.currentPage,
[config.request.pageSize]: this.pageSize,
[config.request.keyword]: this.keyword
}
if(this.onlyImage){
reqData.type = 'image'
}
var res = await config.listApiObj.get(reqData)
var parseData = config.listParseData(res)
this.data = parseData.rows
this.total = parseData.total
this.listLoading = false
this.$refs.scrollbar.setScrollTop(0)
},
//树点击事件
groupClick(data){
this.menuId = data.id
this.currentPage = 1
this.keyword = null
this.getData()
},
//分页刷新表格
reload(){
this.getData()
},
search(){
this.currentPage = 1
this.getData()
},
select(item){
const itemUrl = item[this.fileProps.url]
if(this.multiple){
if(this.value.includes(itemUrl)){
this.value.splice(this.value.findIndex(f => f == itemUrl), 1)
}else{
this.value.push(itemUrl)
}
}else{
if(this.value.includes(itemUrl)){
this.value = ''
}else{
this.value = itemUrl
}
}
},
submit(){
const value = JSON.parse(JSON.stringify(this.value))
this.$emit('update:modelValue', value);
this.$emit('submit', value);
},
//上传处理
uploadChange(file, fileList){
file.tempImg = URL.createObjectURL(file.raw);
this.fileList = fileList
},
uploadBefore(file){
const maxSize = file.size / 1024 / 1024 < this.maxSize;
if (!maxSize) {
this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
return false;
}
},
uploadRequest(param){
var apiObj = config.apiObj;
const data = new FormData();
data.append("file", param.file);
data.append([config.request.menuKey], this.menuId);
apiObj.post(data, {
onUploadProgress: e => {
param.onProgress(e)
}
}).then(res => {
param.onSuccess(res)
}).catch(err => {
param.onError(err)
})
},
uploadProcess(event, file){
file.progress = Number((event.loaded / event.total * 100).toFixed(2))
},
uploadSuccess(res, file){
this.fileList.splice(this.fileList.findIndex(f => f.uid == file.uid), 1)
var response = config.uploadParseData(res);
this.data.unshift({
[this.fileProps.key]: response.id,
[this.fileProps.fileName]: response.fileName,
[this.fileProps.url]: response.url
})
if(!this.multiple){
this.value = response.url
}
},
uploadError(err){
this.$notify.error({
title: '上传文件错误',
message: err
})
},
//内置函数
_isImg(fileUrl){
const imgExt = ['.jpg', '.jpeg', '.png', '.gif', '.bmp']
const fileExt = fileUrl.substring(fileUrl.lastIndexOf("."))
return imgExt.indexOf(fileExt) != -1
},
_getExt(fileUrl){
return fileUrl.substring(fileUrl.lastIndexOf(".") + 1)
}
}
}
</script>
<style scoped>
.sc-file-select {display: flex;}
.sc-file-select__files {flex: 1;}
.sc-file-select__list {height:400px;}
.sc-file-select__item {display: inline-block;float: left;margin:0 15px 25px 0;width:110px;cursor: pointer;}
.sc-file-select__item__file {width:110px;height:110px;position: relative;}
.sc-file-select__item__file .el-image {width:110px;height:110px;}
.sc-file-select__item__box {position: absolute;top:0;right:0;bottom:0;left:0;border: 2px solid var(--el-color-success);z-index: 1;display: none;}
.sc-file-select__item__box::before {content: '';position: absolute;top:0;right:0;bottom:0;left:0;background: var(--el-color-success);opacity: 0.2;display: none;}
.sc-file-select__item:hover .sc-file-select__item__box {display: block;}
.sc-file-select__item.active .sc-file-select__item__box {display: block;}
.sc-file-select__item.active .sc-file-select__item__box::before {display: block;}
.sc-file-select__item p {margin-top: 10px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;-webkit-text-overflow:ellipsis;text-align: center;}
.sc-file-select__item__checkbox {position: absolute;width: 20px;height: 20px;top:7px;right:7px;z-index: 2;background: rgba(0,0,0,0.2);border: 1px solid #fff;display: flex;flex-direction: column;align-items: center;justify-content: center;}
.sc-file-select__item__checkbox i {font-size: 14px;color: #fff;font-weight: bold;display: none;}
.sc-file-select__item__select {position: absolute;width: 20px;height: 20px;top:0px;right:0px;z-index: 2;background: var(--el-color-success);display: none;flex-direction: column;align-items: center;justify-content: center;}
.sc-file-select__item__select i {font-size: 14px;color: #fff;font-weight: bold;}
.sc-file-select__item.active .sc-file-select__item__checkbox {background: var(--el-color-success);}
.sc-file-select__item.active .sc-file-select__item__checkbox i {display: block;}
.sc-file-select__item.active .sc-file-select__item__select {display: flex;}
.sc-file-select__item__file .item-file {width:110px;height:110px;display: flex;flex-direction: column;align-items: center;justify-content: center;}
.sc-file-select__item__file .item-file i {font-size: 40px;}
.sc-file-select__item__file .item-file.item-file-doc {color: #409eff;}
.sc-file-select__item__upload {position: absolute;top:0;right:0;bottom:0;left:0;z-index: 1;background: rgba(255,255,255,0.7);display: flex;flex-direction: column;align-items: center;justify-content: center;}
.sc-file-select__side {width: 200px;margin-right: 15px;border-right: 1px solid rgba(128,128,128,0.2);display: flex;flex-flow: column;}
.sc-file-select__side-menu {flex: 1;}
.sc-file-select__side-msg {height:32px;line-height: 32px;}
.sc-file-select__top {margin-bottom: 15px;display: flex;justify-content: space-between;}
.sc-file-select__upload {display: inline-block;}
.sc-file-select__top .tips {font-size: 12px;margin-left: 10px;color: #999;}
.sc-file-select__top .tips i {font-size: 14px;margin-right: 5px;position: relative;bottom: -0.125em;}
.sc-file-select__pagination {margin:15px 0;}
.sc-file-select__do {text-align: right;}
</style>
@@ -0,0 +1,310 @@
<!--
* @Descripttion: 过滤器V2
* @version: 2.5
* @Author: sakuya
* @Date: 2021年7月30日14:48:41
* @LastEditors: sakuya
* @LastEditTime: 2022年5月13日21:15:44
-->
<template>
<div class="sc-filterBar">
<slot :filterLength="filterObjLength" :openFilter="openFilter">
<el-badge :value="filterObjLength" type="danger" :hidden="filterObjLength<=0">
<el-button icon="el-icon-filter" @click="openFilter"></el-button>
</el-badge>
</slot>
<el-drawer title="过滤器" v-model="drawer" :size="650" append-to-body>
<el-container v-loading="saveLoading">
<el-main style="padding:0">
<el-tabs class="root">
<el-tab-pane lazy>
<template #label>
<div class="tabs-label">过滤项</div>
</template>
<el-scrollbar>
<div class="sc-filter-main">
<h2>设置过滤条件</h2>
<div v-if="filter.length<=0" class="nodata">
没有默认过滤条件请点击增加过滤项
</div>
<table v-else>
<colgroup>
<col width="50">
<col width="140">
<col v-if="showOperator" width="120">
<col>
<col width="40">
</colgroup>
<tr v-for="(item,index) in filter" :key="index">
<td>
<el-tag>{{index+1}}</el-tag>
</td>
<td>
<py-select v-model="item.field" :options="fields" placeholder="过滤字段" filterable @change="fieldChange(item)">
</py-select>
</td>
<td v-if="showOperator">
<el-select v-model="item.operator" placeholder="运算符">
<el-option v-for="ope in item.field.operators || operator" :key="ope.value" :label="ope.label" :value="ope.value"></el-option>
</el-select>
</td>
<td>
<el-input v-if="!item.field.type" v-model="item.value" placeholder="请选择过滤字段" disabled></el-input>
<!-- 输入框 -->
<el-input v-if="item.field.type=='text'" v-model="item.value" :placeholder="item.field.placeholder||'请输入'"></el-input>
<!-- 下拉框 -->
<el-select v-if="item.field.type=='select'" v-model="item.value" :placeholder="item.field.placeholder||'请选择'" filterable :multiple="item.field.extend.multiple" :loading="item.selectLoading" @visible-change="visibleChange($event, item)" :remote="item.field.extend.remote" :remote-method="(query)=>{remoteMethod(query, item)}">
<el-option v-for="field in item.field.extend.data" :key="field.value" :label="field.label" :value="field.value"></el-option>
</el-select>
<!-- 日期 -->
<el-date-picker v-if="item.field.type=='date'" v-model="item.value" type="date" value-format="YYYY-MM-DD" :placeholder="item.field.placeholder||'请选择日期'" style="width: 100%;"></el-date-picker>
<!-- 日期范围 -->
<el-date-picker v-if="item.field.type=='daterange'" v-model="item.value" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 100%;"></el-date-picker>
<!-- 日期时间 -->
<el-date-picker v-if="item.field.type=='datetime'" v-model="item.value" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" :placeholder="item.field.placeholder||'请选择日期'" style="width: 100%;"></el-date-picker>
<!-- 日期时间范围 -->
<el-date-picker v-if="item.field.type=='datetimerange'" v-model="item.value" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 100%;"></el-date-picker>
<!-- 开关 -->
<el-switch v-if="item.field.type=='switch'" v-model="item.value" active-value="1" inactive-value="0"></el-switch>
<!-- 标签 -->
<el-select v-if="item.field.type=='tags'" v-model="item.value" multiple filterable allow-create default-first-option no-data-text="输入关键词后按回车确认" :placeholder="item.field.placeholder||'请输入'"></el-select>
</td>
<td>
<el-icon class="del" @click="delFilter(index)"><el-icon-delete /></el-icon>
</td>
</tr>
</table>
<el-button type="primary" text icon="el-icon-plus" @click="addFilter">增加过滤项</el-button>
</div>
</el-scrollbar>
</el-tab-pane>
<el-tab-pane lazy>
<template #label>
<div class="tabs-label">常用</div>
</template>
<el-scrollbar>
<my ref="my" :data="myFilter" :filterName="filterName" @selectMyfilter="selectMyfilter"></my>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
</el-main>
<el-footer>
<el-button type="primary" @click="ok" :disabled="filter.length<=0">立即过滤</el-button>
<el-button type="primary" plain @click="saveMy" :disabled="filter.length<=0">另存为常用</el-button>
<el-button @click="clear">清空过滤</el-button>
</el-footer>
</el-container>
</el-drawer>
</div>
</template>
<script>
import config from "@/config/filterBar"
import pySelect from './pySelect'
import my from './my'
export default {
name: 'filterBar',
components: {
pySelect,
my
},
props: {
filterName: { type: String, default: "" },
showOperator: { type: Boolean, default: true },
options: { type: Object, default: () => {} }
},
data() {
return {
drawer: false,
operator: config.operator,
fields: this.options,
filter: [],
myFilter: [],
filterObjLength: 0,
saveLoading: false
}
},
computed: {
filterObj(){
const obj = {}
this.filter.forEach((item) => {
obj[item.field.value] = this.showOperator ? `${item.value}${config.separator}${item.operator}` : `${item.value}`
})
return obj
}
},
mounted(){
//默认显示的过滤项
this.fields.forEach((item) => {
if(item.selected){
this.filter.push({
field: item,
operator: item.operator || 'include',
value: ''
})
}
})
},
methods: {
//打开过滤器
openFilter(){
this.drawer = true
},
//增加过滤项
addFilter(){
if(this.fields.length<=0){
this.$message.warning('无过滤项');
return false
}
const filterNum = this.fields[this.filter.length] || this.fields[0]
this.filter.push({
field: filterNum,
operator: filterNum.operator || 'include',
value: ''
})
},
//删除过滤项
delFilter(index){
this.filter.splice(index, 1)
},
//过滤项字段变更事件
fieldChange(tr){
let oldType = tr.field.type
tr.field.type = ''
this.$nextTick(() => {
tr.field.type = oldType
})
tr.operator = tr.field.operator || 'include'
tr.value = ''
},
//下拉框显示事件处理异步
async visibleChange(isopen, item){
if(isopen && item.field.extend.request && !item.field.extend.remote){
item.selectLoading = true;
try {
var data = await item.field.extend.request()
}catch (error) {
console.log(error);
}
item.field.extend.data = data;
item.selectLoading = false;
}
},
//下拉框显示事件处理异步搜索
async remoteMethod(query, item){
if(query !== ''){
item.selectLoading = true;
try {
var data = await item.field.extend.request(query);
}catch (error) {
console.log(error);
}
item.field.extend.data = data;
item.selectLoading = false;
}else{
item.field.extend.data = [];
}
},
//选择常用过滤
selectMyfilter(item){
//常用过滤回显当前过滤项
this.filter = []
this.fields.forEach((field) => {
var filterValue = item.filterObj[field.value]
if(filterValue){
var operator = filterValue.split("|")[1]
var value = filterValue.split("|")[0]
if(field.type=='select' && field.extend.multiple){
value = value.split(",")
}else if(field.type=='daterange'){
value = value.split(",")
}
this.filter.push({
field: field,
operator: operator,
value: value
})
}
})
this.filterObjLength = Object.keys(item.filterObj).length
this.$emit('filterChange',item.filterObj)
this.drawer = false
},
//立即过滤
ok(){
this.filterObjLength = this.filter.length
this.$emit('filterChange',this.filterObj)
this.drawer = false
},
//保存常用
saveMy(){
this.$prompt('常用过滤名称', '另存为常用', {
inputPlaceholder: '请输入识别度较高的常用过滤名称',
inputPattern: /\S/,
inputErrorMessage: '名称不能为空'
})
.then(async ({ value }) => {
this.saveLoading = true
const saveObj = {
title: value,
filterObj: this.filterObj
}
try {
var save = await config.saveMy(this.filterName, saveObj)
}catch (error) {
this.saveLoading = false
console.log(error);
return false
}
if(!save){
return false
}
this.myFilter.push(saveObj)
this.$message.success(`${this.filterName} 保存常用成功`)
this.saveLoading = false
})
.catch(() => {
//
})
},
//清空过滤
clear(){
this.filter = []
this.filterObjLength = 0
this.$emit('filterChange',this.filterObj)
}
}
}
</script>
<style scoped>
.tabs-label {padding:0 20px;}
.nodata {height:46px;line-height: 46px;margin:15px 0;border: 1px dashed #e6e6e6;color: #999;text-align: center;border-radius: 3px;}
.sc-filter-main {padding:20px;border-bottom: 1px solid #e6e6e6;background: #fff;}
.sc-filter-main h2 {font-size: 12px;color: #999;font-weight: normal;}
.sc-filter-main table {width: 100%;margin: 15px 0;}
.sc-filter-main table tr {}
.sc-filter-main table td {padding:5px 10px 5px 0;}
.sc-filter-main table td:deep(.el-input .el-input__inner) {vertical-align: top;}
.sc-filter-main table td .el-select {display: block;}
.sc-filter-main table td .el-date-editor.el-input {display: block;width: 100%;}
.sc-filter-main table td .del {background: #fff;color: #999;width: 32px;height: 32px;line-height: 32px;text-align: center;border-radius:50%;font-size: 12px;cursor: pointer;}
.sc-filter-main table td .del:hover {background: #F56C6C;color: #fff;}
.root {display: flex;height: 100%;flex-direction: column}
.root:deep(.el-tabs__header) {margin: 0;}
.root:deep(.el-tabs__content) {flex: 1;background: #f6f8f9;}
.root:deep(.el-tabs__content) .el-tab-pane{overflow: auto;height:100%;}
.dark .root:deep(.el-tabs__content) {background: var(--el-bg-color-overlay);}
.dark .sc-filter-main {background: var(--el-bg-color);border-color:var(--el-border-color-light);}
.dark .sc-filter-main table td .del {background: none;}
.dark .sc-filter-main table td .del:hover {background: #F56C6C;}
.dark .nodata {border-color:var(--el-border-color-light);}
</style>
@@ -0,0 +1,112 @@
<!--
* @Descripttion: 过滤器V2 常用组件
* @version: 2.0
* @Author: sakuya
* @Date: 2021年7月31日16:49:56
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-filter-my">
<div v-if="loading" class="sc-filter-my-loading">
<el-skeleton :rows="2" animated />
</div>
<template v-else>
<el-empty v-if="myFilter.length<=0" :image-size="100">
<template #description>
<h2>没有常用的过滤</h2>
<p style="margin-top: 10px;max-width: 300px;">常用过滤可以将多个过滤条件保存为一个集合方便下次进行相同条件的过滤</p>
</template>
</el-empty>
<ul v-else class="sc-filter-my-list">
<h2>我的常用过滤</h2>
<li v-for="(item, index) in myFilter" :key="index" @click="selectMyfilter(item)">
<label>{{item.title}}</label>
<el-popconfirm title="确认删除此常用过滤吗?" @confirm="closeMyfilter(item, index)">
<template #reference>
<el-icon class="del" @click.stop="()=>{}"><el-icon-delete /></el-icon>
</template>
</el-popconfirm>
</li>
</ul>
</template>
</div>
</template>
<script>
import config from "@/config/filterBar";
export default {
props: {
filterName: { type: String, default: "" },
data: { type: Object, default: () => {} }
},
data() {
return {
loading: false,
myFilter: []
}
},
watch:{
data: {
handler(){
this.myFilter = this.data
},
deep: true
}
},
mounted() {
this.myFilter = this.data
this.getMyfilter()
},
methods: {
//选择常用过滤
selectMyfilter(item){
this.$emit('selectMyfilter', item)
},
//删除常用过滤
async closeMyfilter(item, index){
try {
var del = await config.delMy(this.filterName)
}catch (error) {
return false
}
if(!del){
return false
}
this.myFilter.splice(index, 1)
this.$message.success('删除常用成功')
},
//远程获取我的常用
async getMyfilter(){
this.loading = true
try {
this.myFilter = await config.getMy(this.filterName)
}catch (error) {
return false
}
this.loading = false
}
}
}
</script>
<style scoped>
.sc-filter-my {}
.sc-filter-my-loading {padding:15px;}
.sc-filter-my-list {list-style-type: none;background: #fff;border-bottom: 1px solid #e6e6e6;}
.sc-filter-my-list h2 {font-size: 12px;color: #999;font-weight: normal;padding:20px;}
.sc-filter-my-list li {padding:12px 20px;cursor: pointer;position: relative;color: #3c4a54;padding-right:80px;}
.sc-filter-my-list li:hover {background: #ecf5ff;color: #409EFF;}
.sc-filter-my-list li label {cursor: pointer;font-size: 14px;line-height: 1.8;}
.sc-filter-my-list li label span {color: #999;margin-right: 10px;}
.sc-filter-my-list li .del {position: absolute;right:20px;top:8px;border-radius:50%;width: 32px;height: 32px;display: flex;align-items: center;justify-content: center;color: #999;}
.sc-filter-my-list li .del:hover {background: #F56C6C;color: #fff;}
[data-theme='dark'] .sc-filter-my .el-empty h2 {color: #fff;}
[data-theme='dark'] .sc-filter-my-list {background: none;border-color:var(--el-border-color-base);}
[data-theme='dark'] .sc-filter-my-list li {color: #d0d0d0;}
[data-theme='dark'] .sc-filter-my-list li:hover {background: var(--el-color-white);}
</style>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,51 @@
<!--
* @Descripttion: 二次封装el-select 支持拼音
* @version: 1.0
* @Author: sakuya
* @Date: 2021年7月31日22:26:56
* @LastEditors:
* @LastEditTime:
-->
<template>
<el-select v-bind="$attrs" :filter-method="filterMethod" @visible-change="visibleChange">
<el-option v-for="field in optionsList" :key="field.value" :label="field.label" :value="field"></el-option>
</el-select>
</template>
<script>
import pinyin from 'pinyin-match'
export default {
props: {
options: { type: Array, default: () => [] }
},
data() {
return {
optionsList: [],
optionsList_: []
}
},
mounted() {
this.optionsList = this.options
this.optionsList_ = [...this.options]
},
methods: {
filterMethod(keyword){
if(keyword){
this.optionsList = this.optionsList_
this.optionsList = this.optionsList.filter((item) =>
pinyin.match(item.label, keyword)
);
}else{
this.optionsList = this.optionsList_
}
},
visibleChange(isopen){
if(isopen){
this.optionsList = this.optionsList_
}
}
}
}
</script>
@@ -0,0 +1,295 @@
<!--
* @Descripttion: 动态表单渲染器
* @version: 1.0
* @Author: sakuya
* @Date: 2021年9月22日09:26:25
* @LastEditors:
* @LastEditTime:
-->
<template>
<el-skeleton v-if="renderLoading || Object.keys(form).length==0" animated />
<el-form v-else ref="form" :model="form" :label-width="config.labelWidth" :label-position="config.labelPosition" v-loading="loading" element-loading-text="Loading...">
<el-row :gutter="15">
<template v-for="(item, index) in config.formItems" :key="index">
<el-col :span="item.span || 24" v-if="!hideHandle(item)">
<sc-title v-if="item.type=='title'" :title="item.label"></sc-title>
<el-form-item v-else :prop="item.name" :rules="rulesHandle(item)">
<template #label>
{{item.label}}
<el-tooltip v-if="item.tips" :content="item.tips">
<el-icon><el-icon-question-filled /></el-icon>
</el-tooltip>
</template>
<!-- input -->
<template v-if="item.type=='input' || item.type=='string'" >
<el-input v-model="form[item.name]" :placeholder="item.options?.placeholder" clearable :maxlength="item.options?.maxlength" show-word-limit :disabled="item.disabled" />
</template>
<!-- input -->
<template v-else-if="item.type=='textarea'" >
<el-input type="textarea" v-model="form[item.name]" :placeholder="item.options?.placeholder" clearable :maxlength="item.options.maxlength" show-word-limit :disabled="item.disabled" />
</template>
<!-- checkbox -->
<template v-else-if="item.type=='checkbox'" >
<template v-if="item.name" >
<el-checkbox v-model="form[item.name][_item.name]" :label="_item.label" v-for="(_item, _index) in item.options.items" :key="_index" :disabled="item.disabled"></el-checkbox>
</template>
<template v-else >
<el-checkbox v-model="form[_item.name]" :label="_item.label" v-for="(_item, _index) in item.options.items" :key="_index" :disabled="item.disabled"></el-checkbox>
</template>
</template>
<!-- checkboxGroup -->
<template v-else-if="item.type=='checkboxGroup'" >
<el-checkbox-group v-model="form[item.name]" :disabled="item.disabled">
<el-checkbox v-for="_item in item.options.items" :key="_item.value" :label="_item.value">{{_item.label}}</el-checkbox>
</el-checkbox-group>
</template>
<!-- upload -->
<template v-else-if="item.type=='upload' || item.type == 'image'" >
<el-form-item :prop="item.name">
<sc-upload v-model="form[item.name]" :title="item.label"></sc-upload>
</el-form-item>
</template>
<!-- switch -->
<template v-else-if="item.type=='switch' || item.type=='boolean'" >
<el-switch v-model="form[item.name]" :active-value="1" :inactive-value="0" :disabled="item.disabled" />
</template>
<!-- select -->
<template v-else-if="item.type=='select'" >
<el-select v-model="form[item.name]" :multiple="item.options.multiple" :placeholder="item.options?.placeholder" clearable filterable style="width: 100%;" :disabled="item.disabled">
<el-option v-for="option in item.options.items" :key="option.value" :label="option.label" :value="option.value"></el-option>
</el-select>
</template>
<template v-else-if="item.type=='scSelect'" >
<sc-select v-model="form[item.name]" :dic="item.options?.dic" :apiObj="item.options?.apiObj" :props="item.options?.props" style="min-width: 240px;" />
</template>
<template v-else-if="item.type=='scSelectTree'" >
<sc-select-tree v-model="form[item.name]" :dic="item.options?.dic" :apiObj="item.options?.apiObj" :props="item.options?.props" style="min-width: 240px;" />
</template>
<!-- cascader -->
<template v-else-if="item.type=='cascader'" >
<el-cascader v-model="form[item.name]" :options="item.options.items" clearable></el-cascader>
</template>
<!-- date -->
<template v-else-if="item.type=='date'" >
<el-date-picker v-model="form[item.name]" :type="item.options.type" :shortcuts="item.options.shortcuts" :default-time="item.options.defaultTime" :value-format="item.options.valueFormat" :placeholder="item.options?.placeholder || '请选择'"></el-date-picker>
</template>
<!-- number -->
<template v-else-if="item.type=='number'" >
<el-input-number v-model="form[item.name]" controls-position="right" :placeholder="item.options?.placeholder" />
</template>
<!-- radio -->
<template v-else-if="item.type=='radio'" >
<el-radio-group v-model="form[item.name]">
<el-radio v-for="_item in item.options.items" :key="_item.value" :label="_item.value">{{_item.label}}</el-radio>
</el-radio-group>
</template>
<!-- color -->
<template v-else-if="item.type=='color'" >
<el-color-picker v-model="form[item.name]" />
</template>
<!-- rate -->
<template v-else-if="item.type=='rate'" >
<el-rate style="margin-top: 6px;" v-model="form[item.name]"></el-rate>
</template>
<!-- slider -->
<template v-else-if="item.type=='slider'" >
<el-slider v-model="form[item.name]" :marks="item.options.marks"></el-slider>
</template>
<!-- tableselect -->
<template v-else-if="item.type=='tableselect'" >
<tableselect-render v-model="form[item.name]" :item="item"></tableselect-render>
</template>
<!-- editor -->
<template v-else-if="item.type=='editor'" >
<sc-editor v-model="form[item.name]" placeholder="请输入" :height="400"></sc-editor>
</template>
<!-- noComponent -->
<template v-else>
<el-tag type="danger">[{{item.type}}] Component not found</el-tag>
</template>
<div v-if="item.message" class="el-form-item-msg">{{item.message}}</div>
</el-form-item>
</el-col>
</template>
<el-col :span="24">
<el-form-item>
<slot>
<el-button type="primary" @click="submit">提交</el-button>
</slot>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
import http from "@/utils/request"
import { defineAsyncComponent } from 'vue'
const tableselectRender = defineAsyncComponent(() => import('./items/tableselect'))
const scEditor = defineAsyncComponent(() => import('@/components/scEditor'))
export default {
props: {
modelValue: { type: Object, default: () => {} },
config: { type: Object, default: () => {} },
loading: { type: Boolean, default: false },
apiObj: { type: Object, default: () => {} },
},
components: {
tableselectRender,
scEditor
},
data() {
return {
form: {},
renderLoading: false
}
},
watch:{
modelValue(){
if(this.hasConfig){
this.deepMerge(this.form, this.modelValue)
}
},
config(){
this.render()
},
form:{
handler(val){
this.$emit("update:modelValue", val)
},
deep: true
}
},
computed: {
hasConfig(){
return Object.keys(this.config).length>0
},
hasValue(){
return Object.keys(this.modelValue).length>0
}
},
created() {
},
mounted() {
if(this.hasConfig){
this.render()
}
},
methods: {
//构建form对象
render() {
this.config.formItems.forEach((item) => {
if(item.type == 'checkbox'){
if(item.name){
const value = {}
item.options.items.forEach((option) => {
value[option.name] = option.value
})
this.form[item.name] = value
}else{
item.options.items.forEach((option) => {
this.form[option.name] = option.value
})
}
}else if(item.type == 'upload'){
if(item.name){
const value = {}
item.options.items.forEach((option) => {
value[option.name] = option.value
})
this.form[item.name] = value
}else{
item.options.items.forEach((option) => {
this.form[option.name] = option.value
})
}
}else{
this.form[item.name] = item.value
}
})
if(this.hasValue){
this.form = this.deepMerge(this.form, this.modelValue)
}
this.getData()
},
//处理远程选项数据
getData() {
this.renderLoading = true
var remoteData = []
this.config.formItems.forEach((item) => {
if(item.options && item.options.remote){
var req = http.get(item.options.remote.api, item.options.remote.data).then(res=>{
item.options.items = res.data
})
remoteData.push(req)
}
})
Promise.all(remoteData).then(()=>{
this.renderLoading = false
})
},
//合并深结构对象
deepMerge(obj1, obj2) {
let key;
for (key in obj2) {
obj1[key] = obj1[key] && obj1[key].toString() === "[object Object]" && (obj2[key] && obj2[key].toString() === "[object Object]") ? this.deepMerge(obj1[key], obj2[key]) : (obj1[key] = obj2[key])
}
return obj1
//return JSON.parse(JSON.stringify(obj1))
},
//处理动态隐藏
hideHandle(item){
if(item.hideHandle){
const exp = eval(item.hideHandle.replace(/\$/g,"this.form"))
return exp
}
return false
},
//处理动态必填
rulesHandle(item){
if(item.requiredHandle){
const exp = eval(item.requiredHandle.replace(/\$/g,"this.form"))
var requiredRule = item.rules.find(t => 'required' in t)
requiredRule.required = exp
}
return item.rules
},
//数据验证
validate(valid, obj){
return this.$refs.form.validate(valid, obj)
},
scrollToField(prop){
return this.$refs.form.scrollToField(prop)
},
resetFields(){
return this.$refs.form.resetFields()
},
//提交
submit(){
if(!this.apiObj){
this.$emit("submit", this.form)
}else{
this.$refs.form.validate().then(async () => {
let res = await this.apiObj.post(this.form)
if(res.code == 1){
this.$emit('update:modelValue', this.form)
this.$emit('onSuccess', res)
}else{
this.$message.error(res.message)
}
}).catch(error => {
this.$message.error('请认真填写信息!')
})
}
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,37 @@
<template>
<sc-table-select v-model="value" :apiObj="apiObj" :table-width="600" :multiple="item.options.multiple" :props="item.options.props" style="width: 100%;">
<el-table-column v-for="(_item, _index) in item.options.column" :key="_index" :prop="_item.prop" :label="_item.label" :width="_item.width"></el-table-column>
</sc-table-select>
</template>
<script>
export default {
name: 'uploadRender',
props: {
modelValue: [String, Number, Boolean, Date, Object, Array],
item: { type: Object, default: () => {} }
},
data() {
return {
value: this.modelValue,
apiObj: this.getApiObj()
}
},
watch:{
value(val){
this.$emit("update:modelValue", val)
}
},
mounted() {
},
methods: {
getApiObj(){
return eval(`this.`+this.item.options.apiObj)
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,123 @@
<!--
* @Descripttion: 表单表格
* @version: 1.3
* @Author: sakuya
* @Date: 2023年2月9日12:32:26
* @LastEditors: sakuya
* @LastEditTime: 2023年2月17日10:41:47
-->
<template>
<div class="sc-form-table" ref="scFormTable">
<el-table :data="data" ref="table" border stripe>
<el-table-column type="index" width="50" fixed="left">
<template #header>
<el-button v-if="!hideAdd" type="primary" icon="el-icon-plus" circle @click="rowAdd"></el-button>
</template>
<template #default="scope">
<div :class="['sc-form-table-handle', {'sc-form-table-handle-delete':!hideDelete}]">
<span>{{scope.$index + 1}}</span>
<el-button v-if="!hideDelete" type="danger" icon="el-icon-delete" plain circle @click="rowDel(scope.row, scope.$index)"></el-button>
</div>
</template>
</el-table-column>
<el-table-column label="" width="50" v-if="dragSort">
<template #default>
<div class="move" style="cursor: move;"><el-icon-d-caret style="width: 1em; height: 1em;"/></div>
</template>
</el-table-column>
<slot></slot>
<template #empty>
{{placeholder}}
</template>
</el-table>
</div>
</template>
<script>
import Sortable from 'sortablejs'
export default {
props: {
modelValue: { type: Array, default: () => [] },
addTemplate: { type: Object, default: () => {} },
placeholder: { type: String, default: "暂无数据" },
dragSort: { type: Boolean, default: false },
hideAdd: { type: Boolean, default: false },
hideDelete: { type: Boolean, default: false }
},
data(){
return {
data: []
}
},
mounted(){
this.data = this.modelValue
if(this.dragSort){
this.rowDrop()
}
},
watch:{
modelValue(){
this.data = this.modelValue
},
data: {
handler(){
this.$emit('update:modelValue', this.data);
},
deep: true
}
},
methods: {
rowDrop(){
const _this = this
const tbody = this.$refs.table.$el.querySelector('.el-table__body-wrapper tbody')
Sortable.create(tbody, {
handle: ".move",
animation: 300,
ghostClass: "ghost",
onEnd({ newIndex, oldIndex }) {
_this.data.splice(newIndex, 0, _this.data.splice(oldIndex, 1)[0])
const newArray = _this.data.slice(0)
const tmpHeight = _this.$refs.scFormTable.offsetHeight
_this.$refs.scFormTable.style.setProperty('height', tmpHeight + 'px')
_this.data = []
_this.$nextTick(() => {
_this.data = newArray
_this.$nextTick(() => {
_this.$refs.scFormTable.style.removeProperty('height')
})
})
}
})
},
rowAdd(){
const temp = JSON.parse(JSON.stringify(this.addTemplate))
this.data.push(temp)
},
rowDel(row, index){
this.data.splice(index, 1)
},
//插入行
pushRow(row){
const temp = row || JSON.parse(JSON.stringify(this.addTemplate))
this.data.push(temp)
},
//根据index删除
deleteRow(index){
this.data.splice(index, 1)
}
}
}
</script>
<style scoped>
.sc-form-table {width: 100%;}
.sc-form-table .sc-form-table-handle {text-align: center;}
.sc-form-table .sc-form-table-handle span {display: inline-block;}
.sc-form-table .sc-form-table-handle button {display: none;}
.sc-form-table .hover-row .sc-form-table-handle-delete span {display: none;}
.sc-form-table .hover-row .sc-form-table-handle-delete button {display: inline-block;}
.sc-form-table .move {text-align: center;font-size: 14px;margin-top: 3px;}
</style>
@@ -0,0 +1,128 @@
<!--
* @Descripttion: 图标选择器组件
* @version: 2.0
* @Author: sakuya
* @Date: 2021年7月27日10:02:46
* @LastEditors: sakuya
* @LastEditTime: 2022年6月6日13:48:49
-->
<template>
<div class="sc-icon-select">
<div class="sc-icon-select__wrapper" :class="{'hasValue':value}" @click="open">
<el-input :prefix-icon="value||'el-icon-plus'" v-model="value" :disabled="disabled" readonly></el-input>
</div>
<el-dialog title="图标选择器" v-model="dialogVisible" :width="760" destroy-on-close append-to-body>
<div class="sc-icon-select__dialog" style="margin: -20px 0 -10px 0;">
<el-form :rules="{}">
<el-form-item prop="searchText">
<el-input class="sc-icon-select__search-input" prefix-icon="el-icon-search" v-model="searchText" placeholder="搜索" clearable/>
</el-form-item>
</el-form>
<el-tabs>
<el-tab-pane v-for="item in data" :key="item.name" lazy>
<template #label>
{{item.name}} <el-tag type="info">{{item.icons.length}}</el-tag>
</template>
<div class="sc-icon-select__list">
<el-scrollbar>
<ul @click="selectIcon">
<el-empty v-if="item.icons.length==0" :image-size="100" description="未查询到相关图标" />
<li v-for="icon in item.icons" :key="icon">
<span :data-icon="icon"></span>
<el-icon><component :is="icon" /></el-icon>
</li>
</ul>
</el-scrollbar>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<el-button @click="clear" text>清除</el-button>
<el-button @click="dialogVisible=false">取消</el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import config from "@/config/iconSelect"
export default {
props: {
modelValue: { type: String, default: "" },
disabled: { type: Boolean, default: false },
},
data() {
return {
value: "",
dialogVisible: false,
data: [],
searchText: ""
}
},
watch:{
modelValue(val){
this.value = val
},
value(val){
this.$emit('update:modelValue', val)
},
searchText(val){
this.search(val)
}
},
mounted() {
this.value = this.modelValue
this.data.push(...config.icons)
},
methods: {
open(){
if(this.disabled){
return false
}
this.dialogVisible = true
},
selectIcon(e){
if(e.target.tagName != 'SPAN'){
return false
}
this.value = e.target.dataset.icon
this.dialogVisible = false
},
clear(){
this.value = ""
this.dialogVisible = false
},
search(text){
if(text){
const filterData = JSON.parse(JSON.stringify(config.icons))
filterData.forEach(t => {
t.icons = t.icons.filter(n => n.includes(text))
})
this.data = filterData
}else{
this.data = JSON.parse(JSON.stringify(config.icons))
}
}
}
}
</script>
<style scoped>
.sc-icon-select {display: inline-flex;}
.sc-icon-select__wrapper {cursor: pointer;display: inline-flex;}
.sc-icon-select__wrapper:deep(.el-input__wrapper).is-focus {box-shadow: 0 0 0 1px var(--el-input-hover-border-color) inset;}
.sc-icon-select__wrapper:deep(.el-input__inner) {flex-grow:0;width: 0;}
.sc-icon-select__wrapper:deep(.el-input__icon) {margin: 0;font-size: 16px;}
.sc-icon-select__wrapper.hasValue:deep(.el-input__icon) {color: var(--el-text-color-regular);}
.sc-icon-select__list {height:270px;overflow: auto;}
.sc-icon-select__list ul {}
.sc-icon-select__list li {display: inline-block;width:80px;height:80px;margin:5px;vertical-align: top;transition: all 0.1s;border-radius: 4px;position: relative;}
.sc-icon-select__list li span {position: absolute;top:0;left:0;right:0;bottom:0;z-index: 1;cursor: pointer;}
.sc-icon-select__list li i {display: inline-block;width: 100%;height:100%;font-size: 26px;color: #6d7882;display: flex;justify-content: center;align-items: center;border-radius: 4px;}
.sc-icon-select__list li:hover {box-shadow: 0 0 1px 4px var(--el-color-primary);background: var(--el-color-primary-light-9);}
.sc-icon-select__list li:hover i {color: var(--el-color-primary);}
</style>
@@ -0,0 +1,78 @@
<template>
<div>
<el-input v-bind="$attrs" v-model="defaultValue" placeholder="请输入关键字" style="width: 100%;" readonly @foucs="showMap" @click="showMap" />
<el-dialog title="地图" v-model="visible" width="800px">
<template #default="scope">
<b-map ref="map" width="100%" height="500px" ak="Mnx3XVAmyovK9wohstKt94nRfDpTy8qd" :center="point.lng ? point : '南昌'" :zoom="zoom" :minZoom="3" :mapType="'BMAP_NORMAL_MAP'" :enableDragging="true" :enableScrollWheelZoom="true" @dblclick="mapClick">
<BCityList :offset="{ x: 20, y: 20 }" />
<b-zoom :offset="{ x: 22, y: 40 }" />
<template v-if="point.lng != ''">
<BMarker :position="point"></BMarker>
<BCircle strokeStyle="solid" strokeColor="#0099ff" :strokeOpacity="0.8" fillColor="#0099ff" :fillOpacity="0.5" :center="point" />
</template>
</b-map>
</template>
</el-dialog>
</div>
</template>
<script>
import { BMap, BZoom, BCityList, BMarker, BCircle } from 'vue3-baidu-map-gl'
export default {
props: {
modelValue: {
type: String,
default: ''
}
},
data() {
return {
visible: false,
point: {lng: '', lat: ''},
zoom: 12,
defaultValue: ''
}
},
mounted() {
this.defaultValue = this.modelValue
},
watch: {
modelValue:{
handler(val) {
this.defaultValue = val
},
deep: true,
immediate: true
}
},
components: {
BMap,
BZoom,
BCityList,
BMarker,
BCircle
},
methods: {
showMap(){
this.visible = true
if(this.defaultValue) {
let arr = this.defaultValue.split(',')
this.point = {
lng: arr[1],
lat: arr[0]
}
}
this.$nextTick(() => {
this.zoom = 20
this.$refs.map.resetCenter()
})
},
mapClick(e) {
this.$emit('update:modelValue', e.latlng.lat+','+e.latlng.lng)
this.visible = false
}
}
}
</script>
@@ -0,0 +1,47 @@
<!--
* @Descripttion: 状态指示器
* @version: 1.0
* @Author: sakuya
* @Date: 2021年11月11日09:30:12
* @LastEditors:
* @LastEditTime:
-->
<template>
<span class="sc-state" :class="[{'sc-status-processing':pulse}, 'sc-state-bg--'+type]"></span>
</template>
<script>
export default {
props: {
type: { type: String, default: "primary" },
pulse: { type: Boolean, default: false }
}
}
</script>
<style scoped>
.sc-state {display: inline-block;background: #000;width: 8px;height: 8px;border-radius: 50%;vertical-align: middle;}
.sc-status-processing {position: relative;}
.sc-status-processing:after {position: absolute;top:0px;left:0px;width: 100%;height: 100%;border-radius: 50%;background: inherit;content: '';animation: warn 1.2s ease-in-out infinite;}
.sc-state-bg--primary {background: var(--el-color-primary);}
.sc-state-bg--success {background: var(--el-color-success);}
.sc-state-bg--warning {background: var(--el-color-warning);}
.sc-state-bg--danger {background: var(--el-color-danger);}
.sc-state-bg--info {background: var(--el-color-info);}
@keyframes warn {
0% {
transform: scale(0.5);
opacity: 1;
}
30% {
opacity: 0.7;
}
100% {
transform: scale(2.5);
opacity: 0;
}
}
</style>
@@ -0,0 +1,66 @@
<!--
* @Descripttion: 趋势标记
* @version: 1.0
* @Author: sakuya
* @Date: 2021年11月11日11:07:10
* @LastEditors:
* @LastEditTime:
-->
<template>
<span class="sc-trend" :class="'sc-trend--'+type">
<el-icon v-if="iconType=='P'" class="sc-trend-icon"><el-icon-top /></el-icon>
<el-icon v-if="iconType=='N'" class="sc-trend-icon"><el-icon-bottom /></el-icon>
<el-icon v-if="iconType=='Z'" class="sc-trend-icon"><el-icon-right /></el-icon>
<em class="sc-trend-prefix">{{prefix}}</em>
<em class="sc-trend-value">{{modelValue}}</em>
<em class="sc-trend-suffix">{{suffix}}</em>
</span>
</template>
<script>
export default {
props: {
modelValue: { type: Number, default: 0 },
prefix: { type: String, default: "" },
suffix: { type: String, default: "" },
reverse: { type: Boolean, default: false }
},
computed: {
absValue(){
return Math.abs(this.modelValue);
},
iconType(v){
if(this.modelValue == 0){
v = 'Z'
}else if(this.modelValue < 0){
v = 'N'
}else if(this.modelValue > 0){
v = 'P'
}
return v
},
type(v){
if(this.modelValue == 0){
v = 'Z'
}else if(this.modelValue < 0){
v = this.reverse?'P':'N'
}else if(this.modelValue > 0){
v = this.reverse?'N':'P'
}
return v
}
}
}
</script>
<style scoped>
.sc-trend {display: flex;align-items: center;}
.sc-trend-icon {margin-right: 2px;}
.sc-trend em {font-style: normal;}
.sc-trend-prefix {margin-right: 2px;}
.sc-trend-suffix {margin-left: 2px;}
.sc-trend--P {color: #f56c6c;}
.sc-trend--N {color: #67c23a;}
.sc-trend--Z {color: #555;}
</style>
@@ -0,0 +1,52 @@
<!--
* @Descripttion: 页面头部样式组件
* @version: 1.0
* @Author: sakuya
* @Date: 2021年7月20日08:49:07
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-page-header">
<div v-if="icon" class="sc-page-header__icon">
<span>
<el-icon><component :is="icon" /></el-icon>
</span>
</div>
<div class="sc-page-header__title">
<h2>{{ title }}</h2>
<p v-if="description || $slots.default">
<slot>
{{ description }}
</slot>
</p>
</div>
<div v-if="$slots.main" class="sc-page-header__main">
<slot name="main"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: { type: String, required: true, default: "" },
description: { type: String, default: "" },
icon: { type: String, default: "" },
}
}
</script>
<style scoped>
.sc-page-header {background: #fff;border-bottom: 1px solid #e6e6e6;padding:20px 25px;display: flex;}
.sc-page-header__icon {width: 50px;}
.sc-page-header__icon span {display: inline-block;width: 30px;height: 30px;background: #409EFF;border-radius: 40%;display: flex;align-items: center;justify-content: center;}
.sc-page-header__icon span i {color: #fff;font-size: 14px;}
.sc-page-header__title {flex: 1;}
.sc-page-header__title h2 {font-size: 17px;color: #3c4a54;font-weight: bold;margin-top: 3px;}
.sc-page-header__title p {font-size: 13px;color: #999;margin-top: 15px;}
[data-theme='dark'] .sc-page-header {background:#2b2b2b ;border-color:var(--el-border-color-base);}
[data-theme='dark'] .sc-page-header__title h2 {color: #d0d0d0;}
</style>
@@ -0,0 +1,92 @@
<!--
* @Descripttion: 密码强度检测
* @version: 1.0
* @Author: sakuya
* @Date: 2022年6月2日15:36:01
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-password-strength">
<div class="sc-password-strength-bar" :class="`sc-password-strength-level-${level}`"></div>
</div>
</template>
<script>
export default {
props: {
modelValue: { type: String, default: "" },
},
data() {
return {
level: 0
}
},
watch: {
modelValue() {
this.strength(this.modelValue)
}
},
mounted() {
this.strength(this.modelValue)
},
methods: {
strength(v){
var _level = 0
//长度
var has_length = v.length >= 6
//包含数字
var has_number = /\d/.test(v)
//包含小写英文
var has_lovercase = /[a-z]/.test(v)
//包含大写英文
var has_uppercase = /[A-Z]/.test(v)
//没有连续的字符3位
var no_continuity = !/(\w)\1{2}/.test(v)
//包含特殊字符
var has_special = /[`~!@#$%^&*()_+<>?:"{},./;'[\]]/.test(v)
if(v.length <= 0){
_level = 0
this.level = _level
return false
}
if(!has_length){
_level = 1
this.level = _level
return false
}
if(has_number){
_level += 1
}
if(has_lovercase){
_level += 1
}
if(has_uppercase){
_level += 1
}
if(no_continuity){
_level += 1
}
if(has_special){
_level += 1
}
this.level = _level
}
}
}
</script>
<style scoped>
.sc-password-strength {height: 5px;width: 100%;background: var(--el-color-info-light-5);border-radius: 5px;position: relative;margin:10px 0;}
.sc-password-strength:before {left: 20%;}
.sc-password-strength:after {right: 20%;}
.sc-password-strength:before, .sc-password-strength:after {position: absolute;content: "";display: block;width: 20%;height: inherit;border: 5px solid var(--el-bg-color-overlay);border-top: 0;border-bottom: 0;z-index: 1;background-color: transparent;box-sizing: border-box;}
.sc-password-strength-bar {position: absolute;height: inherit;width: 0%;border-radius: inherit;transition: width .5s ease-in-out,background .25s;background: transparent;}
.sc-password-strength-level-1 {width: 20%;background-color: var(--el-color-error);}
.sc-password-strength-level-2 {width: 40%;background-color: var(--el-color-error);}
.sc-password-strength-level-3 {width: 60%;background-color: var(--el-color-warning);}
.sc-password-strength-level-4 {width: 80%;background-color: var(--el-color-success);}
.sc-password-strength-level-5 {width: 100%;background-color: var(--el-color-success);}
</style>
@@ -0,0 +1,88 @@
<!--
* @Descripttion: 生成二维码组件
* @version: 1.0
* @Author: sakuya
* @Date: 2021年12月20日14:22:20
* @LastEditors:
* @LastEditTime:
-->
<template>
<img ref="img"/>
</template>
<script>
import QRcode from "./qrcode"
export default {
props: {
text: { type: String, required: true, default: "" },
size: { type: Number, default: 100 },
logo: { type: String, default: "" },
logoSize: { type: Number, default: 30 },
logoPadding: { type: Number, default: 5 },
colorDark: { type: String, default: "#000000" },
colorLight: { type: String, default: "#ffffff" },
correctLevel: { type: Number, default: 2 },
},
data() {
return {
qrcode: null
}
},
watch:{
text(){
this.draw()
}
},
mounted() {
this.draw()
},
methods: {
//创建原始二维码DOM
async create(){
return new Promise((resolve) => {
var element = document.createElement("div");
new QRcode(element, {
text: this.text,
width: this.size,
height: this.size,
colorDark: this.colorDark,
colorLight: this.colorLight,
correctLevel: this.correctLevel
})
if (element.getElementsByTagName("canvas")[0]) {
this.qrcode = element
resolve()
}
})
},
//绘制LOGO
async drawLogo(){
return new Promise((resolve) => {
var logo = new Image()
logo.src = this.logo
const logoPos = (this.size - this.logoSize) / 2
const rectSize = this.logoSize + this.logoPadding
const rectPos = (this.size - rectSize) / 2
var ctx = this.qrcode.getElementsByTagName("canvas")[0].getContext("2d")
logo.onload = ()=>{
ctx.fillRect(rectPos, rectPos, rectSize, rectSize)
ctx.drawImage(logo, logoPos, logoPos, this.logoSize, this.logoSize)
resolve()
}
})
},
async draw(){
await this.create()
if(this.logo){
await this.drawLogo()
}
this.$refs.img.src = this.qrcode.getElementsByTagName("canvas")[0].toDataURL("image/png")
},
}
}
</script>
<style>
</style>
@@ -0,0 +1,618 @@
/**
* @fileoverview
* - Using the 'QRCode for Javascript library'
* - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
* - this library has no dependencies.
* - source page: https://github.com/makevoid/qrcodejs
*
*
* @author davidshimjs
* @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
* @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
*/
var QRCode;
(function () {
//---------------------------------------------------------------------
// QRCode for JavaScript
//
// Copyright (c) 2009 Kazuhiko Arase
//
// URL: http://www.d-project.com/
//
// Licensed under the MIT license:
// http://www.opensource.org/licenses/mit-license.php
//
// The word "QR Code" is registered trademark of
// DENSO WAVE INCORPORATED
// http://www.denso-wave.com/qrcode/faqpatent-e.html
//
//---------------------------------------------------------------------
function QR8bitByte(data) {
this.mode = QRMode.MODE_8BIT_BYTE;
this.data = data;
this.parsedData = [];
// Added to support UTF-8 Characters
for (var i = 0, l = this.data.length; i < l; i++) {
var byteArray = [];
var code = this.data.charCodeAt(i);
if (code > 0x10000) {
byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18);
byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12);
byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6);
byteArray[3] = 0x80 | (code & 0x3F);
} else if (code > 0x800) {
byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12);
byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6);
byteArray[2] = 0x80 | (code & 0x3F);
} else if (code > 0x80) {
byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6);
byteArray[1] = 0x80 | (code & 0x3F);
} else {
byteArray[0] = code;
}
this.parsedData.push(byteArray);
}
this.parsedData = Array.prototype.concat.apply([], this.parsedData);
if (this.parsedData.length != this.data.length) {
this.parsedData.unshift(191);
this.parsedData.unshift(187);
this.parsedData.unshift(239);
}
}
QR8bitByte.prototype = {
getLength: function (buffer) {
return this.parsedData.length;
},
write: function (buffer) {
for (var i = 0, l = this.parsedData.length; i < l; i++) {
buffer.put(this.parsedData[i], 8);
}
}
};
function QRCodeModel(typeNumber, errorCorrectLevel) {
this.typeNumber = typeNumber;
this.errorCorrectLevel = errorCorrectLevel;
this.modules = null;
this.moduleCount = 0;
this.dataCache = null;
this.dataList = [];
}
QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);}
return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row<this.moduleCount;row++){this.modules[row]=new Array(this.moduleCount);for(var col=0;col<this.moduleCount;col++){this.modules[row][col]=null;}}
this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(test,maskPattern);if(this.typeNumber>=7){this.setupTypeNumber(test);}
if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);}
this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}}
return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row<this.modules.length;row++){var y=row*cs;for(var col=0;col<this.modules[row].length;col++){var x=col*cs;var dark=this.modules[row][col];if(dark){qr_mc.beginFill(0,100);qr_mc.moveTo(x,y);qr_mc.lineTo(x+cs,y);qr_mc.lineTo(x+cs,y+cs);qr_mc.lineTo(x,y+cs);qr_mc.endFill();}}}
return qr_mc;},setupTimingPattern:function(){for(var r=8;r<this.moduleCount-8;r++){if(this.modules[r][6]!=null){continue;}
this.modules[r][6]=(r%2==0);}
for(var c=8;c<this.moduleCount-8;c++){if(this.modules[6][c]!=null){continue;}
this.modules[6][c]=(c%2==0);}},setupPositionAdjustPattern:function(){var pos=QRUtil.getPatternPosition(this.typeNumber);for(var i=0;i<pos.length;i++){for(var j=0;j<pos.length;j++){var row=pos[i];var col=pos[j];if(this.modules[row][col]!=null){continue;}
for(var r=-2;r<=2;r++){for(var c=-2;c<=2;c++){if(r==-2||r==2||c==-2||c==2||(r==0&&c==0)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}}}},setupTypeNumber:function(test){var bits=QRUtil.getBCHTypeNumber(this.typeNumber);for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;}
for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}}
for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}}
this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex<data.length){dark=(((data[byteIndex]>>>bitIndex)&1)==1);}
var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;}
this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}}
row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;i<dataList.length;i++){var data=dataList[i];buffer.put(data.mode,4);buffer.put(data.getLength(),QRUtil.getLengthInBits(data.mode,typeNumber));data.write(buffer);}
var totalDataCount=0;for(var i=0;i<rsBlocks.length;i++){totalDataCount+=rsBlocks[i].dataCount;}
if(buffer.getLengthInBits()>totalDataCount*8){throw new Error("code length overflow. ("
+buffer.getLengthInBits()
+">"
+totalDataCount*8
+")");}
if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);}
while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);}
while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;}
buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;}
buffer.put(QRCodeModel.PAD1,8);}
return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r<rsBlocks.length;r++){var dcCount=rsBlocks[r].dataCount;var ecCount=rsBlocks[r].totalCount-dcCount;maxDcCount=Math.max(maxDcCount,dcCount);maxEcCount=Math.max(maxEcCount,ecCount);dcdata[r]=new Array(dcCount);for(var i=0;i<dcdata[r].length;i++){dcdata[r][i]=0xff&buffer.buffer[i+offset];}
offset+=dcCount;var rsPoly=QRUtil.getErrorCorrectPolynomial(ecCount);var rawPoly=new QRPolynomial(dcdata[r],rsPoly.getLength()-1);var modPoly=rawPoly.mod(rsPoly);ecdata[r]=new Array(rsPoly.getLength()-1);for(var i=0;i<ecdata[r].length;i++){var modIndex=i+modPoly.getLength()-ecdata[r].length;ecdata[r][i]=(modIndex>=0)?modPoly.get(modIndex):0;}}
var totalCodeCount=0;for(var i=0;i<rsBlocks.length;i++){totalCodeCount+=rsBlocks[i].totalCount;}
var data=new Array(totalCodeCount);var index=0;for(var i=0;i<maxDcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<dcdata[r].length){data[index++]=dcdata[r][i];}}}
for(var i=0;i<maxEcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<ecdata[r].length){data[index++]=ecdata[r][i];}}}
return data;};var QRMode={MODE_NUMBER:1<<0,MODE_ALPHA_NUM:1<<1,MODE_8BIT_BYTE:1<<2,MODE_KANJI:1<<3};var QRErrorCorrectLevel={L:1,M:0,Q:3,H:2};var QRMaskPattern={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var QRUtil={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:(1<<10)|(1<<8)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0),G18:(1<<12)|(1<<11)|(1<<10)|(1<<9)|(1<<8)|(1<<5)|(1<<2)|(1<<0),G15_MASK:(1<<14)|(1<<12)|(1<<10)|(1<<4)|(1<<1),getBCHTypeInfo:function(data){var d=data<<10;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)>=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));}
return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));}
return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;}
return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i<errorCorrectLength;i++){a=a.multiply(new QRPolynomial([1,QRMath.gexp(i)],0));}
return a;},getLengthInBits:function(mode,type){if(1<=type&&type<10){switch(mode){case QRMode.MODE_NUMBER:return 10;case QRMode.MODE_ALPHA_NUM:return 9;case QRMode.MODE_8BIT_BYTE:return 8;case QRMode.MODE_KANJI:return 8;default:throw new Error("mode:"+mode);}}else if(type<27){switch(mode){case QRMode.MODE_NUMBER:return 12;case QRMode.MODE_ALPHA_NUM:return 11;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 10;default:throw new Error("mode:"+mode);}}else if(type<41){switch(mode){case QRMode.MODE_NUMBER:return 14;case QRMode.MODE_ALPHA_NUM:return 13;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 12;default:throw new Error("mode:"+mode);}}else{throw new Error("type:"+type);}},getLostPoint:function(qrCode){var moduleCount=qrCode.getModuleCount();var lostPoint=0;for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount;col++){var sameCount=0;var dark=qrCode.isDark(row,col);for(var r=-1;r<=1;r++){if(row+r<0||moduleCount<=row+r){continue;}
for(var c=-1;c<=1;c++){if(col+c<0||moduleCount<=col+c){continue;}
if(r==0&&c==0){continue;}
if(dark==qrCode.isDark(row+r,col+c)){sameCount++;}}}
if(sameCount>5){lostPoint+=(3+sameCount-5);}}}
for(var row=0;row<moduleCount-1;row++){for(var col=0;col<moduleCount-1;col++){var count=0;if(qrCode.isDark(row,col))count++;if(qrCode.isDark(row+1,col))count++;if(qrCode.isDark(row,col+1))count++;if(qrCode.isDark(row+1,col+1))count++;if(count==0||count==4){lostPoint+=3;}}}
for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount-6;col++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row,col+1)&&qrCode.isDark(row,col+2)&&qrCode.isDark(row,col+3)&&qrCode.isDark(row,col+4)&&!qrCode.isDark(row,col+5)&&qrCode.isDark(row,col+6)){lostPoint+=40;}}}
for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount-6;row++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row+1,col)&&qrCode.isDark(row+2,col)&&qrCode.isDark(row+3,col)&&qrCode.isDark(row+4,col)&&!qrCode.isDark(row+5,col)&&qrCode.isDark(row+6,col)){lostPoint+=40;}}}
var darkCount=0;for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount;row++){if(qrCode.isDark(row,col)){darkCount++;}}}
var ratio=Math.abs(100*darkCount/moduleCount/moduleCount-50)/5;lostPoint+=ratio*10;return lostPoint;}};var QRMath={glog:function(n){if(n<1){throw new Error("glog("+n+")");}
return QRMath.LOG_TABLE[n];},gexp:function(n){while(n<0){n+=255;}
while(n>=256){n-=255;}
return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<<i;}
for(var i=8;i<256;i++){QRMath.EXP_TABLE[i]=QRMath.EXP_TABLE[i-4]^QRMath.EXP_TABLE[i-5]^QRMath.EXP_TABLE[i-6]^QRMath.EXP_TABLE[i-8];}
for(var i=0;i<255;i++){QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]]=i;}
function QRPolynomial(num,shift){if(num.length==undefined){throw new Error(num.length+"/"+shift);}
var offset=0;while(offset<num.length&&num[offset]==0){offset++;}
this.num=new Array(num.length-offset+shift);for(var i=0;i<num.length-offset;i++){this.num[i]=num[i+offset];}}
QRPolynomial.prototype={get:function(index){return this.num[index];},getLength:function(){return this.num.length;},multiply:function(e){var num=new Array(this.getLength()+e.getLength()-1);for(var i=0;i<this.getLength();i++){for(var j=0;j<e.getLength();j++){num[i+j]^=QRMath.gexp(QRMath.glog(this.get(i))+QRMath.glog(e.get(j)));}}
return new QRPolynomial(num,0);},mod:function(e){if(this.getLength()-e.getLength()<0){return this;}
var ratio=QRMath.glog(this.get(0))-QRMath.glog(e.get(0));var num=new Array(this.getLength());for(var i=0;i<this.getLength();i++){num[i]=this.get(i);}
for(var i=0;i<e.getLength();i++){num[i]^=QRMath.gexp(QRMath.glog(e.get(i))+ratio);}
return new QRPolynomial(num,0).mod(e);}};function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this.dataCount=dataCount;}
QRRSBlock.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];QRRSBlock.getRSBlocks=function(typeNumber,errorCorrectLevel){var rsBlock=QRRSBlock.getRsBlockTable(typeNumber,errorCorrectLevel);if(rsBlock==undefined){throw new Error("bad rs block @ typeNumber:"+typeNumber+"/errorCorrectLevel:"+errorCorrectLevel);}
var length=rsBlock.length/3;var list=[];for(var i=0;i<length;i++){var count=rsBlock[i*3+0];var totalCount=rsBlock[i*3+1];var dataCount=rsBlock[i*3+2];for(var j=0;j<count;j++){list.push(new QRRSBlock(totalCount,dataCount));}}
return list;};QRRSBlock.getRsBlockTable=function(typeNumber,errorCorrectLevel){switch(errorCorrectLevel){case QRErrorCorrectLevel.L:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+0];case QRErrorCorrectLevel.M:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+1];case QRErrorCorrectLevel.Q:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+2];case QRErrorCorrectLevel.H:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+3];default:return undefined;}};function QRBitBuffer(){this.buffer=[];this.length=0;}
QRBitBuffer.prototype={get:function(index){var bufIndex=Math.floor(index/8);return((this.buffer[bufIndex]>>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i<length;i++){this.putBit(((num>>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);}
if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));}
this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]];
function _isSupportCanvas() {
return typeof CanvasRenderingContext2D != "undefined";
}
// android 2.x doesn't support Data-URI spec
function _getAndroid() {
var android = false;
var sAgent = navigator.userAgent;
if (/android/i.test(sAgent)) { // android
android = true;
var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i);
if (aMat && aMat[1]) {
android = parseFloat(aMat[1]);
}
}
return android;
}
var svgDrawer = (function() {
var Drawing = function (el, htOption) {
this._el = el;
this._htOption = htOption;
};
Drawing.prototype.draw = function (oQRCode) {
var _htOption = this._htOption;
var _el = this._el;
var nCount = oQRCode.getModuleCount();
var nWidth = Math.floor(_htOption.width / nCount);
var nHeight = Math.floor(_htOption.height / nCount);
this.clear();
function makeSVG(tag, attrs) {
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs)
if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]);
return el;
}
var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight});
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
_el.appendChild(svg);
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"}));
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"}));
for (var row = 0; row < nCount; row++) {
for (var col = 0; col < nCount; col++) {
if (oQRCode.isDark(row, col)) {
var child = makeSVG("use", {"x": String(col), "y": String(row)});
child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template")
svg.appendChild(child);
}
}
}
};
Drawing.prototype.clear = function () {
while (this._el.hasChildNodes())
this._el.removeChild(this._el.lastChild);
};
return Drawing;
})();
var useSVG = document.documentElement.tagName.toLowerCase() === "svg";
// Drawing in DOM by using Table tag
var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () {
var Drawing = function (el, htOption) {
this._el = el;
this._htOption = htOption;
};
/**
* Draw the QRCode
*
* @param {QRCode} oQRCode
*/
Drawing.prototype.draw = function (oQRCode) {
var _htOption = this._htOption;
var _el = this._el;
var nCount = oQRCode.getModuleCount();
var nWidth = Math.floor(_htOption.width / nCount);
var nHeight = Math.floor(_htOption.height / nCount);
var aHTML = ['<table style="border:0;border-collapse:collapse;">'];
for (var row = 0; row < nCount; row++) {
aHTML.push('<tr>');
for (var col = 0; col < nCount; col++) {
aHTML.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + nWidth + 'px;height:' + nHeight + 'px;background-color:' + (oQRCode.isDark(row, col) ? _htOption.colorDark : _htOption.colorLight) + ';"></td>');
}
aHTML.push('</tr>');
}
aHTML.push('</table>');
_el.innerHTML = aHTML.join('');
// Fix the margin values as real size.
var elTable = _el.childNodes[0];
var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2;
var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2;
if (nLeftMarginTable > 0 && nTopMarginTable > 0) {
elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px";
}
};
/**
* Clear the QRCode
*/
Drawing.prototype.clear = function () {
this._el.innerHTML = '';
};
return Drawing;
})() : (function () { // Drawing in Canvas
function _onMakeImage() {
this._elImage.src = this._elCanvas.toDataURL("image/png");
this._elImage.style.display = "block";
this._elCanvas.style.display = "none";
}
// Android 2.1 bug workaround
// http://code.google.com/p/android/issues/detail?id=5141
if (this && this._android && this._android <= 2.1) {
var factor = 1 / window.devicePixelRatio;
var drawImage = CanvasRenderingContext2D.prototype.drawImage;
CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) {
if (("nodeName" in image) && /img/i.test(image.nodeName)) {
for (var i = arguments.length - 1; i >= 1; i--) {
arguments[i] = arguments[i] * factor;
}
} else if (typeof dw == "undefined") {
arguments[1] *= factor;
arguments[2] *= factor;
arguments[3] *= factor;
arguments[4] *= factor;
}
drawImage.apply(this, arguments);
};
}
/**
* Check whether the user's browser supports Data URI or not
*
* @private
* @param {Function} fSuccess Occurs if it supports Data URI
* @param {Function} fFail Occurs if it doesn't support Data URI
*/
function _safeSetDataURI(fSuccess, fFail) {
var self = this;
self._fFail = fFail;
self._fSuccess = fSuccess;
// Check it just once
if (self._bSupportDataURI === null) {
var el = document.createElement("img");
var fOnError = function() {
self._bSupportDataURI = false;
if (self._fFail) {
self._fFail.call(self);
}
};
var fOnSuccess = function() {
self._bSupportDataURI = true;
if (self._fSuccess) {
self._fSuccess.call(self);
}
};
el.onabort = fOnError;
el.onerror = fOnError;
el.onload = fOnSuccess;
el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data.
return;
} else if (self._bSupportDataURI === true && self._fSuccess) {
self._fSuccess.call(self);
} else if (self._bSupportDataURI === false && self._fFail) {
self._fFail.call(self);
}
};
/**
* Drawing QRCode by using canvas
*
* @constructor
* @param {HTMLElement} el
* @param {Object} htOption QRCode Options
*/
var Drawing = function (el, htOption) {
this._bIsPainted = false;
this._android = _getAndroid();
this._htOption = htOption;
this._elCanvas = document.createElement("canvas");
this._elCanvas.width = htOption.width;
this._elCanvas.height = htOption.height;
el.appendChild(this._elCanvas);
this._el = el;
this._oContext = this._elCanvas.getContext("2d");
this._bIsPainted = false;
this._elImage = document.createElement("img");
this._elImage.alt = "Scan me!";
this._elImage.style.display = "none";
this._el.appendChild(this._elImage);
this._bSupportDataURI = null;
};
/**
* Draw the QRCode
*
* @param {QRCode} oQRCode
*/
Drawing.prototype.draw = function (oQRCode) {
var _elImage = this._elImage;
var _oContext = this._oContext;
var _htOption = this._htOption;
var nCount = oQRCode.getModuleCount();
var nWidth = _htOption.width / nCount;
var nHeight = _htOption.height / nCount;
var nRoundedWidth = Math.round(nWidth);
var nRoundedHeight = Math.round(nHeight);
_elImage.style.display = "none";
this.clear();
for (var row = 0; row < nCount; row++) {
for (var col = 0; col < nCount; col++) {
var bIsDark = oQRCode.isDark(row, col);
var nLeft = col * nWidth;
var nTop = row * nHeight;
_oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
_oContext.lineWidth = 1;
_oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
_oContext.fillRect(nLeft, nTop, nWidth, nHeight);
// 안티 앨리어싱 방지 처리
_oContext.strokeRect(
Math.floor(nLeft) + 0.5,
Math.floor(nTop) + 0.5,
nRoundedWidth,
nRoundedHeight
);
_oContext.strokeRect(
Math.ceil(nLeft) - 0.5,
Math.ceil(nTop) - 0.5,
nRoundedWidth,
nRoundedHeight
);
}
}
this._bIsPainted = true;
};
/**
* Make the image from Canvas if the browser supports Data URI.
*/
Drawing.prototype.makeImage = function () {
if (this._bIsPainted) {
_safeSetDataURI.call(this, _onMakeImage);
}
};
/**
* Return whether the QRCode is painted or not
*
* @return {Boolean}
*/
Drawing.prototype.isPainted = function () {
return this._bIsPainted;
};
/**
* Clear the QRCode
*/
Drawing.prototype.clear = function () {
this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height);
this._bIsPainted = false;
};
/**
* @private
* @param {Number} nNumber
*/
Drawing.prototype.round = function (nNumber) {
if (!nNumber) {
return nNumber;
}
return Math.floor(nNumber * 1000) / 1000;
};
return Drawing;
})();
/**
* Get the type by string length
*
* @private
* @param {String} sText
* @param {Number} nCorrectLevel
* @return {Number} type
*/
function _getTypeNumber(sText, nCorrectLevel) {
var nType = 1;
var length = _getUTF8Length(sText);
for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
var nLimit = 0;
switch (nCorrectLevel) {
case QRErrorCorrectLevel.L :
nLimit = QRCodeLimitLength[i][0];
break;
case QRErrorCorrectLevel.M :
nLimit = QRCodeLimitLength[i][1];
break;
case QRErrorCorrectLevel.Q :
nLimit = QRCodeLimitLength[i][2];
break;
case QRErrorCorrectLevel.H :
nLimit = QRCodeLimitLength[i][3];
break;
}
if (length <= nLimit) {
break;
} else {
nType++;
}
}
if (nType > QRCodeLimitLength.length) {
throw new Error("Too long data");
}
return nType;
}
function _getUTF8Length(sText) {
var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a');
return replacedText.length + (replacedText.length != sText ? 3 : 0);
}
/**
* @class QRCode
* @constructor
* @example
* new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie");
*
* @example
* var oQRCode = new QRCode("test", {
* text : "http://naver.com",
* width : 128,
* height : 128
* });
*
* oQRCode.clear(); // Clear the QRCode.
* oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode.
*
* @param {HTMLElement|String} el target element or 'id' attribute of element.
* @param {Object|String} vOption
* @param {String} vOption.text QRCode link data
* @param {Number} [vOption.width=256]
* @param {Number} [vOption.height=256]
* @param {String} [vOption.colorDark="#000000"]
* @param {String} [vOption.colorLight="#ffffff"]
* @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H]
*/
QRCode = function (el, vOption) {
this._htOption = {
width : 256,
height : 256,
typeNumber : 4,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRErrorCorrectLevel.H
};
if (typeof vOption === 'string') {
vOption = {
text : vOption
};
}
// Overwrites options
if (vOption) {
for (var i in vOption) {
this._htOption[i] = vOption[i];
}
}
if (typeof el == "string") {
el = document.getElementById(el);
}
if (this._htOption.useSVG) {
Drawing = svgDrawer;
}
this._android = _getAndroid();
this._el = el;
this._oQRCode = null;
this._oDrawing = new Drawing(this._el, this._htOption);
if (this._htOption.text) {
this.makeCode(this._htOption.text);
}
};
/**
* Make the QRCode
*
* @param {String} sText link data
*/
QRCode.prototype.makeCode = function (sText) {
this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel);
this._oQRCode.addData(sText);
this._oQRCode.make();
this._el.title = sText;
this._oDrawing.draw(this._oQRCode);
this.makeImage();
};
/**
* Make the Image from Canvas element
* - It occurs automatically
* - Android below 3 doesn't support Data-URI spec.
*
* @private
*/
QRCode.prototype.makeImage = function () {
if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) {
this._oDrawing.makeImage();
}
};
/**
* Clear the QRCode
*/
QRCode.prototype.clear = function () {
this._oDrawing.clear();
};
/**
* @name QRCode.CorrectLevel
*/
QRCode.CorrectLevel = QRErrorCorrectLevel;
})();
export default QRCode;
@@ -0,0 +1,94 @@
<!--
* @Descripttion: 异步选择器
* @version: 1.1
* @Author: sakuya
* @Date: 2021年8月3日15:53:37
* @LastEditors: sakuya
* @LastEditTime: 2023年2月23日15:17:24
-->
<template>
<div class="sc-select">
<div v-if="initloading" class="sc-select-loading">
<el-icon class="is-loading"><el-icon-loading /></el-icon>
</div>
<el-select v-bind="$attrs" :loading="loading" @visible-change="visibleChange">
<el-option v-for="item in options" :key="item[props.value]" :label="item[props.label]" :value="objValueType ? item : item[props.value]">
<slot name="option" :data="item"></slot>
</el-option>
</el-select>
</div>
</template>
<script>
import config from "@/config/select";
export default {
props: {
apiObj: { type: Object, default: () => {} },
dic: { type: String, default: "" },
objValueType: { type: Boolean, default: false },
params: { type: Object, default: () => ({}) },
props: { type: Object, default: () => ({
label: config.props.label,
value: config.props.value
}) }
},
data() {
return {
dicParams: this.params,
loading: false,
options: [],
initloading: false
}
},
created() {
//如果有默认值就去请求接口获取options
if(this.hasValue()){
this.initloading = true
this.getRemoteData()
}
},
methods: {
//选项显示隐藏事件
visibleChange(ispoen){
if(ispoen && this.options.length==0 && (this.dic || this.apiObj)){
this.getRemoteData()
}
},
//获取数据
async getRemoteData(){
this.loading = true
this.dicParams[config.request.name] = this.dic
var res = {}
if(this.apiObj){
res = await this.apiObj.get(this.params)
}else if(this.dic){
res = await config.dicApiObj.get(this.params)
}
var response = config.parseData(res)
this.options = response.data
this.loading = false
this.initloading = false
},
//判断是否有回显默认值
hasValue(){
if(Array.isArray(this.$attrs.modelValue) && this.$attrs.modelValue.length <= 0){
return false
}else if(this.$attrs.modelValue){
return true
}else{
return false
}
}
}
}
</script>
<style scoped>
.sc-select {display: inline-block;position: relative;}
.sc-select-loading {position: absolute;top:0;left:0;right:0;bottom:0;background: #fff;z-index: 100;border-radius: 5px;border: 1px solid #EBEEF5;display: flex;align-items: center;padding-left:10px;}
.sc-select-loading i {font-size: 14px;}
.dark .sc-select-loading {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
</style>
@@ -0,0 +1,127 @@
<!--
* @Descripttion: 分类筛选器
* @version: 1.0
* @Author: sakuya
* @Date: 2022年5月26日15:59:52
* @LastEditors:
* @LastEditTime:
-->
<template>
<div class="sc-select-filter">
<div v-if="data.length<=0" class="sc-select-filter__no-data">
暂无数据
</div>
<div v-for="item in data" :key="item.key" class="sc-select-filter__item">
<div class="sc-select-filter__item-title" :style="{'width':labelWidth+'px'}"><label>{{item.title}}</label></div>
<div class="sc-select-filter__item-options">
<ul>
<li :class="{'active':selected[item.key]&&selected[item.key].includes(option.value)}" v-for="option in item.options" :key="option.value" @click="select(option, item)">
<el-icon v-if="option.icon"><component :is="option.icon" /></el-icon>
<span>{{option.label}}</span>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
data: { type: Array, default: () => [] },
selectedValues: { type: Object, default: () => { return {} } },
labelWidth: {type: Number, default: 80},
outputValueTypeToArray: { type: Boolean, default: false }
},
data() {
return {
selected: {}
}
},
watch:{
data(val) {
val.forEach(item => {
this.selected[item.key] = this.selectedValues[item.key] ||
(Array.isArray(item.options) && item.options.length) ? [item.options[0].value] : []
})
}
},
computed: {
selectedString() {
var outputData = JSON.parse(JSON.stringify(this.selected))
for (var key in outputData) {
outputData[key] = outputData[key].join(",")
}
return outputData
}
},
mounted() {
//默认赋值
this.data.forEach(item => {
this.selected[item.key] = this.selectedValues[item.key] ||
(Array.isArray(item.options) && item.options.length) ? [item.options[0].value] : []
})
},
methods: {
select(option, item){
//判断单选多选
if(item.multiple){
//如果多选选择的第一个
if(option.value === item.options[0].value){
//就赋值第一个的值
this.selected[item.key] = [option.value]
}else{
//如果选择的值已有
if(this.selected[item.key].includes(option.value)){
//删除选择的值
this.selected[item.key].splice(this.selected[item.key].findIndex(s => s === option.value), 1)
//当全删光时,把第一个选中
if(this.selected[item.key].length == 0){
this.selected[item.key] = [item.options[0].value]
}
}else{
//未有值的时候,追加选中值
this.selected[item.key].push(option.value)
//当含有第一个的值的时候,把第一个删除
if(this.selected[item.key].includes(item.options[0].value)){
this.selected[item.key].splice(this.selected[item.key].findIndex(s => s === item.options[0].value), 1)
}
}
}
}else{
//单选时,如果点击了已有值就赋值
if(!this.selected[item.key].includes(option.value)){
this.selected[item.key] = [option.value]
}else{
return false
}
}
this.change()
},
change(){
if(this.outputValueTypeToArray){
this.$emit('onChange', this.selected)
}else{
this.$emit('onChange', this.selectedString)
}
}
}
}
</script>
<style scoped>
.sc-select-filter {width: 100%;}
.sc-select-filter__item {display: flex;}
.sc-select-filter__item-title {width: 80px;}
.sc-select-filter__item-title label {font-size: 14px;padding-top:13px;display: inline-block;color: #999;}
.sc-select-filter__item-options {flex: 1;border-bottom: 1px dashed var(--el-border-color-light);}
.sc-select-filter__item-options ul {display: flex;flex-wrap: wrap;padding-top: 10px;}
.sc-select-filter__item-options li {list-style: none;cursor: pointer;height:28px;padding:0 15px;border-radius:32px;margin: 0 10px 10px 0;display: flex;align-items: center;background: var(--el-color-primary-light-9);}
.sc-select-filter__item-options li .el-icon {margin-right: 3px;font-size: 16px;}
.sc-select-filter__item-options li:hover {color: var(--el-color-primary);}
.sc-select-filter__item-options li.active {background: var(--el-color-primary);color: #fff;font-weight: bold;}
.sc-select-filter__item:last-of-type .sc-select-filter__item-options {border: 0;}
.sc-select-filter__no-data {color: #999;}
</style>
@@ -0,0 +1,48 @@
<template>
<el-select v-bind="$attrs" remote-show-suffix clearable remote :remote-method="querySearchAsync" :loading="loading">
<el-option v-for="item in options" :key="item[props.value]" :label="item[props.label]" :value="item[props.value]"/>
</el-select>
</template>
<script>
export default{
props: {
apiObj: { type: Object, default: () => {} },
props: { type: Object, default: () => {return {label: 'name', value: 'id'}} }
},
data(){
return {
loading: false,
options: []
}
},
created() {
//如果有默认值就去请求接口获取options
if(this.hasValue()){
this.initloading = true
}
},
methods: {
async querySearchAsync(queryString){
let params = {}
params[this.props.value] = queryString
let res = await this.apiObj.get(params)
if(res.code == 1){
this.options = res.data
this.loading = false
}
},
//判断是否有回显默认值
hasValue(){
if(Array.isArray(this.$attrs.modelValue) && this.$attrs.modelValue.length <= 0){
return false
}else if(this.$attrs.modelValue){
this.querySearchAsync(this.$attrs.modelValue)
return true
}else{
return false
}
}
}
}
</script>
@@ -0,0 +1,45 @@
<template>
<div>
<el-tree-select v-bind="$attrs" :data="treeData" check-strictly :render-after-expand="false" :default-expand-all="true" :props="defaultProps" clearable style="min-width: 240px;" />
</div>
</template>
<script>
import config from '@/config/select.js'
export default {
props: {
apiObj: { type: Object, default: () => {} },
props: { type: Object, default: () => {} },
hasTop: { type: Boolean, default: false }
},
data(){
return {
treeData: [],
defaultProps: {label: config.props.label, value: config.props.value}
}
},
mounted(){
this.defaultProps = Object.assign(this.defaultProps, this.props);
this.$nextTick(() => {
this.getData()
})
},
methods:{
async getData(){
let data = {}
data[this.defaultProps.label] = '顶级'
data[this.defaultProps.value] = 0
let res = await this.apiObj.get({is_tree: 1})
if(res.code == 1){
this.treeData = this.hasTop ? [data, ...res.data] : res.data
}else{
this.treeData = this.hasTop ? [data] : []
}
},
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,70 @@
<!--
* @Descripttion: 统计数值组件
* @version: 1.1
* @Author: sakuya
* @Date: 2021年6月23日13:11:32
* @LastEditors: sakuya
* @LastEditTime: 2022年5月14日19:55:09
-->
<template>
<div class="sc-statistic">
<div class="sc-statistic-title">
{{ title }}
<el-tooltip v-if="tips" effect="light">
<template #content>
<div style="width: 200px;line-height: 2;">
{{ tips }}
</div>
</template>
<el-icon class="sc-statistic-tips"><el-icon-question-filled/></el-icon>
</el-tooltip>
</div>
<div class="sc-statistic-content">
<span v-if="prefix" class="sc-statistic-content-prefix">{{ prefix }}</span>
<span class="sc-statistic-content-value">{{ cmtValue }}</span>
<span v-if="suffix" class="sc-statistic-content-suffix">{{ suffix }}</span>
</div>
<div v-if="description || $slots.default" class="sc-statistic-description">
<slot>
{{ description }}
</slot>
</div>
</div>
</template>
<script>
export default {
props: {
title: { type: String, required: true, default: "" },
value: { type: String, required: true, default: "" },
prefix: { type: String, default: "" },
suffix: { type: String, default: "" },
description: { type: String, default: "" },
tips: { type: String, default: "" },
groupSeparator: { type: Boolean, default: false }
},
data() {
return {
}
},
computed: {
cmtValue(){
return this.groupSeparator ? this.$TOOL.groupSeparator(this.value) : this.value
}
}
}
</script>
<style scoped>
.sc-statistic-title {font-size: 12px;color: #999;margin-bottom: 10px;display: flex;align-items: center;}
.sc-statistic-tips {margin-left: 5px;}
.sc-statistic-content {font-size: 20px;color: #333;}
.sc-statistic-content-value {font-weight: bold;}
.sc-statistic-content-prefix {margin-right: 5px;}
.sc-statistic-content-suffix {margin-left: 5px;font-size: 12px;}
.sc-statistic-description {margin-top: 10px;color: #999;}
.dark .sc-statistic-content {color: #d0d0d0;}
</style>
@@ -0,0 +1,23 @@
import { h, resolveComponent } from 'vue'
export default {
render() {
return h (
resolveComponent("el-table-column"),
{
index: this.index,
...this.$attrs
},
this.$slots
)
},
methods: {
index(index){
if(this.$attrs.type=="index"){
let page = this.$parent.$parent.currentPage
let pageSize = this.$parent.$parent.pageSize
return (page - 1) * pageSize + index + 1
}
}
}
}
@@ -0,0 +1,120 @@
<template>
<div v-if="usercolumn.length>0" class="setting-column" v-loading="isSave">
<div class="setting-column__title">
<span class="move_b"></span>
<span class="show_b">显示</span>
<span class="name_b">名称</span>
<span class="width_b">宽度</span>
<span class="sortable_b">排序</span>
<span class="fixed_b">固定</span>
</div>
<div class="setting-column__list" ref="list">
<ul>
<li v-for="item in usercolumn" :key="item.prop">
<span class="move_b">
<el-tag class="move" style="cursor: move;"><el-icon-d-caret style="width: 1em; height: 1em;"/></el-tag>
</span>
<span class="show_b">
<el-switch v-model="item.hide" :active-value="false" :inactive-value="true"></el-switch>
</span>
<span class="name_b" :title="item.prop">{{ item.label }}</span>
<span class="width_b">
<el-input v-model="item.width" placeholder="auto"></el-input>
</span>
<span class="sortable_b">
<el-switch v-model="item.sortable"></el-switch>
</span>
<span class="fixed_b">
<el-switch v-model="item.fixed"></el-switch>
</span>
</li>
</ul>
</div>
<div class="setting-column__bottom">
<el-button @click="backDefaul" :disabled="isSave">重置</el-button>
<el-button @click="save" type="primary">保存</el-button>
</div>
</div>
<el-empty v-else description="暂无可配置的列" :image-size="80"></el-empty>
</template>
<script>
import Sortable from 'sortablejs'
export default {
components: {
Sortable
},
props: {
column: { type: Object, default: () => {} }
},
data() {
return {
isSave: false,
usercolumn: JSON.parse(JSON.stringify(this.column||[]))
}
},
watch:{
usercolumn: {
handler(){
this.$emit('userChange', this.usercolumn)
},
deep: true
}
},
mounted() {
this.usercolumn.length>0 && this.rowDrop()
},
methods: {
rowDrop(){
const _this = this
const tbody = this.$refs.list.querySelector('ul')
Sortable.create(tbody, {
handle: ".move",
animation: 300,
ghostClass: "ghost",
onEnd({ newIndex, oldIndex }) {
const tableData = _this.usercolumn
const currRow = tableData.splice(oldIndex, 1)[0]
tableData.splice(newIndex, 0, currRow)
}
})
},
backDefaul(){
this.$emit('back', this.usercolumn)
},
save(){
this.$emit('save', this.usercolumn)
}
}
}
</script>
<style scoped>
.setting-column {}
.setting-column__title {border-bottom: 1px solid #EBEEF5;padding-bottom:15px;}
.setting-column__title span {display: inline-block;font-weight: bold;color: #909399;font-size: 12px;}
.setting-column__title span.move_b {width: 30px;margin-right:15px;}
.setting-column__title span.show_b {width: 60px;}
.setting-column__title span.name_b {width: 140px;}
.setting-column__title span.width_b {width: 60px;margin-right:15px;}
.setting-column__title span.sortable_b {width: 60px;}
.setting-column__title span.fixed_b {width: 60px;}
.setting-column__list {max-height:314px;overflow: auto;}
.setting-column__list li {list-style: none;margin:10px 0;display: flex;align-items: center;}
.setting-column__list li>span {display: inline-block;font-size: 12px;}
.setting-column__list li span.move_b {width: 30px;margin-right:15px;}
.setting-column__list li span.show_b {width: 60px;}
.setting-column__list li span.name_b {width: 140px;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;cursor:default;}
.setting-column__list li span.width_b {width: 60px;margin-right:15px;}
.setting-column__list li span.sortable_b {width: 60px;}
.setting-column__list li span.fixed_b {width: 60px;}
.setting-column__list li.ghost {opacity: 0.3;}
.setting-column__bottom {border-top: 1px solid #EBEEF5;padding-top:15px;text-align: right;}
.dark .setting-column__title {border-color: var(--el-border-color-light);}
.dark .setting-column__bottom {border-color: var(--el-border-color-light);}
</style>
@@ -0,0 +1,409 @@
<!--
* @Descripttion: 数据表格组件
* @version: 1.10
* @Author: sakuya
* @Date: 2021年11月29日21:51:15
* @LastEditors: sakuya
* @LastEditTime: 2022年6月4日17:35:26
-->
<template>
<div class="scTable" :style="{'height':_height}" ref="scTableMain" v-loading="loading">
<div class="scTable-table" :style="{'height':_table_height}">
<el-table v-bind="$attrs" :data="tableData" :row-key="rowKey" :key="toggleIndex" ref="scTable" :height="height=='auto'?null:'100%'" :size="config.size" :border="config.border" :stripe="config.stripe" :summary-method="remoteSummary?remoteSummaryMethod:summaryMethod" @sort-change="sortChange" @filter-change="filterChange">
<slot></slot>
<template v-for="(item, index) in userColumn" :key="index">
<el-table-column v-if="!item.hide" :column-key="item.prop" :label="item.label" :prop="item.prop" :width="item.width" :sortable="item.sortable" :fixed="item.fixed" :filters="item.filters" :filter-method="remoteFilter||!item.filters?null:filterHandler" :show-overflow-tooltip="item.showOverflowTooltip">
<template #default="scope">
<slot :name="item.prop" v-bind="scope">
{{scope.row[item.prop]}}
</slot>
</template>
</el-table-column>
</template>
<el-table-column min-width="1"></el-table-column>
<template #empty>
<el-empty :description="emptyText" :image-size="100"></el-empty>
</template>
</el-table>
</div>
<div class="scTable-page" v-if="!hidePagination || !hideDo">
<div class="scTable-pagination">
<el-pagination v-if="!hidePagination" background :layout="paginationLayout" :total="total" :page-size="scPageSize" :page-sizes="pageSizes" v-model:currentPage="currentPage" @current-change="paginationChange" @update:page-size="pageSizeChange"></el-pagination>
</div>
<div class="scTable-do" v-if="!hideDo">
<el-button v-if="!hideRefresh" @click="refresh" icon="el-icon-refresh" circle style="margin-left:15px"></el-button>
<el-popover v-if="column" placement="top" title="列设置" :width="500" trigger="click" :hide-after="0" @show="customColumnShow=true" @after-leave="customColumnShow=false">
<template #reference>
<el-button icon="el-icon-set-up" circle style="margin-left:15px"></el-button>
</template>
<columnSetting v-if="customColumnShow" ref="columnSetting" @userChange="columnSettingChange" @save="columnSettingSave" @back="columnSettingBack" :column="userColumn"></columnSetting>
</el-popover>
<el-popover v-if="!hideSetting" placement="top" title="表格设置" :width="400" trigger="click" :hide-after="0">
<template #reference>
<el-button icon="el-icon-setting" circle style="margin-left:15px"></el-button>
</template>
<el-form label-width="80px" label-position="left">
<el-form-item label="表格尺寸">
<el-radio-group v-model="config.size" @change="configSizeChange">
<el-radio-button value="large"></el-radio-button>
<el-radio-button value="default">正常</el-radio-button>
<el-radio-button value="small"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="样式">
<el-checkbox v-model="config.border" label="纵向边框"></el-checkbox>
<el-checkbox v-model="config.stripe" label="斑马纹"></el-checkbox>
</el-form-item>
</el-form>
</el-popover>
</div>
</div>
</div>
</template>
<script>
import config from "@/config/table";
import columnSetting from './columnSetting'
export default {
name: 'scTable',
components: {
columnSetting
},
props: {
tableName: { type: String, default: "" },
apiObj: { type: Object, default: () => {} },
params: { type: Object, default: () => ({}) },
data: { type: Object, default: () => {} },
height: { type: [String,Number], default: "100%" },
size: { type: String, default: "small" },
border: { type: Boolean, default: true },
stripe: { type: Boolean, default: false },
pageSize: { type: Number, default: config.pageSize },
pageSizes: { type: Array, default: config.pageSizes },
rowKey: { type: String, default: "" },
summaryMethod: { type: Function, default: null },
column: { type: Object, default: () => {} },
remoteSort: { type: Boolean, default: false },
remoteFilter: { type: Boolean, default: false },
remoteSummary: { type: Boolean, default: false },
hidePagination: { type: Boolean, default: false },
hideDo: { type: Boolean, default: false },
hideRefresh: { type: Boolean, default: false },
hideSetting: { type: Boolean, default: false },
paginationLayout: { type: String, default: config.paginationLayout },
},
watch: {
//监听从props里拿到值了
data(){
this.tableData = this.data;
this.total = this.tableData.length;
},
apiObj(){
this.tableParams = this.params;
this.refresh();
},
column(){
this.userColumn=this.column;
}
},
computed: {
_height() {
return Number(this.height)?Number(this.height)+'px':this.height
},
_table_height() {
return this.hidePagination && this.hideDo ? "100%" : "calc(100% - 50px)"
}
},
data() {
return {
scPageSize: this.pageSize,
isActivat: true,
emptyText: "暂无数据",
toggleIndex: 0,
tableData: [],
total: 0,
currentPage: 1,
prop: null,
order: null,
loading: false,
tableHeight:'100%',
tableParams: this.params,
userColumn: [],
customColumnShow: false,
summary: {},
config: {
size: this.size,
border: this.border,
stripe: this.stripe
}
}
},
mounted() {
//判断是否开启自定义列
if(this.column){
this.getCustomColumn()
}else{
this.userColumn = this.column
}
//判断是否静态数据
if(this.apiObj){
this.getData();
}else if(this.data){
this.tableData = this.data;
this.total = this.tableData.length
}
},
activated(){
if(!this.isActivat){
this.$refs.scTable.doLayout()
}
},
deactivated(){
this.isActivat = false;
},
methods: {
//获取列
async getCustomColumn(){
const userColumn = await config.columnSettingGet(this.tableName, this.column)
this.userColumn = userColumn
},
//获取数据
async getData(){
this.tableData = []
this.loading = true;
var reqData = {
[config.request.page]: this.currentPage,
[config.request.pageSize]: this.scPageSize,
[config.request.prop]: this.prop,
[config.request.order]: this.order
}
if(this.hidePagination){
delete reqData[config.request.page]
delete reqData[config.request.pageSize]
}
Object.assign(reqData, this.tableParams)
try {
var res = await this.apiObj.get(reqData);
}catch(error){
this.loading = false;
this.emptyText = error.statusText;
return false;
}
try {
var response = config.parseData(res);
}catch(error){
this.loading = false;
this.emptyText = "数据格式错误";
return false;
}
if(response.code != config.successCode){
this.loading = false;
this.emptyText = response.msg;
}else{
this.emptyText = "暂无数据";
if(this.hidePagination){
this.tableData = response.data || [];
}else{
this.tableData = response.rows || [];
}
this.total = response.total || 0;
this.summary = response.summary || {};
this.loading = false;
}
// this.$refs.scTable.setScrollTop(0)
this.$emit('dataChange', res, this.tableData)
},
//分页点击
paginationChange(){
this.getData();
},
//条数变化
pageSizeChange(size){
this.scPageSize = size
this.getData();
},
//刷新数据
refresh(){
this.$refs.scTable.clearSelection();
this.getData();
},
//更新数据 合并上一次params
upData(params, page=1){
this.currentPage = page;
this.$refs.scTable.clearSelection();
Object.assign(this.tableParams, params || {})
this.getData()
},
//重载数据 替换params
reload(params, page=1){
this.currentPage = page;
this.tableParams = params || {}
this.$refs.scTable.clearSelection();
this.$refs.scTable.clearSort()
this.$refs.scTable.clearFilter()
this.getData()
},
//自定义变化事件
columnSettingChange(userColumn){
this.userColumn = userColumn;
this.toggleIndex += 1;
},
//自定义列保存
async columnSettingSave(userColumn){
this.$refs.columnSetting.isSave = true
try {
await config.columnSettingSave(this.tableName, userColumn)
}catch(error){
this.$message.error('保存失败')
this.$refs.columnSetting.isSave = false
}
this.$message.success('保存成功')
this.$refs.columnSetting.isSave = false
},
//自定义列重置
async columnSettingBack(){
this.$refs.columnSetting.isSave = true
try {
const column = await config.columnSettingReset(this.tableName, this.column)
this.userColumn = column
this.$refs.columnSetting.usercolumn = JSON.parse(JSON.stringify(this.userColumn||[]))
}catch(error){
this.$message.error('重置失败')
this.$refs.columnSetting.isSave = false
}
this.$refs.columnSetting.isSave = false
},
//排序事件
sortChange(obj){
if(!this.remoteSort){
return false
}
if(obj.column && obj.prop){
this.prop = obj.prop
this.order = obj.order
}else{
this.prop = null
this.order = null
}
this.getData()
},
//本地过滤
filterHandler(value, row, column){
const property = column.property;
return row[property] === value;
},
//过滤事件
filterChange(filters){
if(!this.remoteFilter){
return false
}
Object.keys(filters).forEach(key => {
filters[key] = filters[key].join(',')
})
this.upData(filters)
},
//远程合计行处理
remoteSummaryMethod(param){
const {columns} = param
const sums = []
columns.forEach((column, index) => {
if(index === 0) {
sums[index] = '合计'
return
}
const values = this.summary[column.property]
if(values){
sums[index] = values
}else{
sums[index] = ''
}
})
return sums
},
configSizeChange(){
this.$refs.scTable.doLayout()
},
//插入行 unshiftRow
unshiftRow(row){
this.tableData.unshift(row)
},
//插入行 pushRow
pushRow(row){
this.tableData.push(row)
},
//根据key覆盖数据
updateKey(row, rowKey=this.rowKey){
this.tableData.filter(item => item[rowKey]===row[rowKey] ).forEach(item => {
Object.assign(item, row)
})
},
//根据index覆盖数据
updateIndex(row, index){
Object.assign(this.tableData[index], row)
},
//根据index删除
removeIndex(index){
this.tableData.splice(index, 1)
},
//根据index批量删除
removeIndexes(indexes=[]){
indexes.forEach(index => {
this.tableData.splice(index, 1)
})
},
//根据key删除
removeKey(key, rowKey=this.rowKey){
this.tableData.splice(this.tableData.findIndex(item => item[rowKey]===key), 1)
},
//根据keys批量删除
removeKeys(keys=[], rowKey=this.rowKey){
keys.forEach(key => {
this.tableData.splice(this.tableData.findIndex(item => item[rowKey]===key), 1)
})
},
//原生方法转发
clearSelection(){
this.$refs.scTable.clearSelection()
},
toggleRowSelection(row, selected){
this.$refs.scTable.toggleRowSelection(row, selected)
},
toggleAllSelection(){
this.$refs.scTable.toggleAllSelection()
},
toggleRowExpansion(row, expanded){
this.$refs.scTable.toggleRowExpansion(row, expanded)
},
setCurrentRow(row){
this.$refs.scTable.setCurrentRow(row)
},
clearSort(){
this.$refs.scTable.clearSort()
},
clearFilter(columnKey){
this.$refs.scTable.clearFilter(columnKey)
},
doLayout(){
this.$refs.scTable.doLayout()
},
sort(prop, order){
this.$refs.scTable.sort(prop, order)
}
}
}
</script>
<style scoped>
.scTable {}
.scTable-table {height: calc(100% - 50px);}
.scTable-page {height:50px;display: flex;align-items: center;justify-content: space-between;padding:0 15px;}
.scTable-do {white-space: nowrap;}
.scTable:deep(.el-table__footer) .cell {font-weight: bold;}
.scTable:deep(.el-table__body-wrapper) .el-scrollbar__bar.is-horizontal {height: 12px;border-radius: 12px;}
.scTable:deep(.el-table__body-wrapper) .el-scrollbar__bar.is-vertical {width: 12px;border-radius: 12px;}
</style>
@@ -0,0 +1,233 @@
<!--
* @Descripttion: 表格选择器组件
* @version: 1.3
* @Author: sakuya
* @Date: 2021年6月10日10:04:07
* @LastEditors: sakuya
* @LastEditTime: 2022年6月6日21:50:36
-->
<template>
<el-select ref="select" v-model="defaultLable" :size="size" :clearable="clearable" :multiple="multiple" :collapse-tags="collapseTags" :collapse-tags-tooltip="collapseTagsTooltip" :filterable="filterable" :placeholder="placeholder" :disabled="disabled" :filter-method="filterMethod" @remove-tag="removeTag" @visible-change="visibleChange" @clear="clear">
<template #header>
<slot name="header" :form="formData" :submit="formSubmit"></slot>
</template>
<template #empty>
<div :style="{width: tableWidth+'px'}" v-loading="loading">
<el-table ref="table" :data="tableData" :height="245" :highlight-current-row="!multiple" @row-click="click" @select="select" @select-all="selectAll">
<el-table-column v-if="multiple" type="selection" width="45"></el-table-column>
<el-table-column v-else type="index" width="45">
<template #default="scope"><span>{{scope.$index+(currentPage - 1) * pageSize + 1}}</span></template>
</el-table-column>
<slot></slot>
</el-table>
</div>
</template>
<template #footer>
<el-pagination small background layout="prev, pager, next" :total="total" :page-size="pageSize" v-model:currentPage="currentPage" @current-change="reload"></el-pagination>
</template>
</el-select>
</template>
<script>
import config from "@/config/tableSelect";
export default {
props: {
modelValue: null,
apiObj: { type: Object, default: () => {} },
params: { type: Object, default: () => {} },
placeholder: { type: String, default: "请选择" },
size: { type: String, default: "default" },
clearable: { type: Boolean, default: false },
multiple: { type: Boolean, default: false },
filterable: { type: Boolean, default: false },
collapseTags: { type: Boolean, default: false },
collapseTagsTooltip: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
tableWidth: {type: Number, default: 400},
mode: { type: String, default: "popover" },
props: { type: Object, default: () => {} }
},
data() {
return {
loading: false,
keyword: null,
defaultValue: [],
defaultLable: [],
tableData: [],
pageSize: config.pageSize,
total: 0,
currentPage: 1,
defaultProps: {
label: config.props.label,
value: config.props.value,
page: config.request.page,
pageSize: config.request.pageSize,
keyword: config.request.keyword
},
formData: {}
}
},
computed: {
},
watch: {
modelValue:{
handler(){
this.defaultValue = this.modelValue
this.autoCurrentLabel()
},
deep: true
}
},
mounted() {
this.defaultProps = Object.assign(this.defaultProps, this.props);
this.defaultValue = this.modelValue
this.autoCurrentLabel()
},
methods: {
//表格显示隐藏回调
visibleChange(visible){
if(visible){
this.currentPage = 1
this.keyword = null
this.formData = {}
this.getData()
}else{
this.autoCurrentLabel()
}
},
//获取表格数据
async getData(){
this.loading = true;
var reqData = {
[this.defaultProps.page]: this.currentPage,
[this.defaultProps.pageSize]: this.pageSize,
[this.defaultProps.keyword]: this.keyword
}
Object.assign(reqData, this.params, this.formData)
var res = await this.apiObj.get(reqData);
var parseData = config.parseData(res)
this.tableData = parseData.rows;
this.total = parseData.total;
this.loading = false;
//表格默认赋值
this.$nextTick(() => {
if(this.multiple){
this.defaultValue.forEach(row => {
var setrow = this.tableData.filter(item => item[this.defaultProps.value]===row[this.defaultProps.value] )
if(setrow.length > 0){
this.$refs.table.toggleRowSelection(setrow[0], true);
}
})
}else{
var setrow = this.tableData.filter(item => item[this.defaultProps.value]===this.defaultValue[this.defaultProps.value] )
this.$refs.table.setCurrentRow(setrow[0]);
}
this.$refs.table.setScrollTop(0)
})
},
//插糟表单提交
formSubmit(){
this.currentPage = 1
this.keyword = null
this.getData()
},
//分页刷新表格
reload(){
this.getData()
},
//自动模拟options赋值
autoCurrentLabel(){
this.$nextTick(() => {
if(this.multiple){
this.defaultValue.map(item => {
this.defaultLable.push(item[this.props.label])
})
}else{
this.defaultLable = this.defaultValue[this.defaultProps.label]
}
})
},
//表格勾选事件
select(rows, row){
var isSelect = rows.length && rows.indexOf(row) !== -1
if(isSelect){
this.defaultValue.push(row)
}else{
this.defaultValue.splice(this.defaultValue.findIndex(item => item[this.defaultProps.value] == row[this.defaultProps.value]), 1)
}
this.autoCurrentLabel()
this.$emit('update:modelValue', this.defaultValue);
this.$emit('change', this.defaultValue);
},
//表格全选事件
selectAll(rows){
var isAllSelect = rows.length > 0
if(isAllSelect){
rows.forEach(row => {
var isHas = this.defaultValue.find(item => item[this.defaultProps.value] == row[this.defaultProps.value])
if(!isHas){
this.defaultValue.push(row)
}
})
}else{
this.tableData.forEach(row => {
var isHas = this.defaultValue.find(item => item[this.defaultProps.value] == row[this.defaultProps.value])
if(isHas){
this.defaultValue.splice(this.defaultValue.findIndex(item => item[this.defaultProps.value] == row[this.defaultProps.value]), 1)
}
})
}
this.autoCurrentLabel()
this.$emit('update:modelValue', this.defaultValue);
this.$emit('change', this.defaultValue);
},
click(row){
if(this.multiple){
//处理多选点击行
}else{
this.defaultValue = row
this.$refs.select.blur()
this.autoCurrentLabel()
this.$emit('update:modelValue', this.defaultValue);
this.$emit('change', this.defaultValue);
}
},
//tags删除后回调
removeTag(tag){
var row = this.findRowByKey(tag[this.defaultProps.value])
this.$refs.table.toggleRowSelection(row, false);
this.$emit('update:modelValue', this.defaultValue);
},
//清空后的回调
clear(){
this.$emit('update:modelValue', this.defaultValue);
},
// 关键值查询表格数据行
findRowByKey (value) {
return this.tableData.find(item => item[this.defaultProps.value] === value)
},
filterMethod(keyword){
if(!keyword){
this.keyword = null;
return false;
}
this.keyword = keyword;
this.getData()
},
// 触发select隐藏
blur(){
this.$refs.select.blur();
},
// 触发select显示
focus(){
this.$refs.select.focus();
}
}
}
</script>
<style scoped>
</style>
@@ -0,0 +1,27 @@
<template>
<div class="sc-title">
<span class="title">{{title}}</span>
<span><slot name="right"></slot></span>
</div>
</template>
<script>
export default {
props: {
title: { type: String, required: true, default: "" },
},
data() {
return {
}
},
computed: {
}
}
</script>
<style scoped>
.sc-title {border-bottom: 1px solid #eee;padding: 5px; margin: 15px 0 5px 0; display: flex;justify-content: space-between;}
.sc-title span.title{font-size: 14px; color: #3c4a54;font-weight: bold;}
</style>
@@ -0,0 +1,193 @@
<template>
<div class="sc-upload-file">
<el-upload
:disabled="disabled"
:auto-upload="autoUpload"
:action="action"
:name="name"
:data="data"
:http-request="request"
v-model:file-list="defaultFileList"
:show-file-list="showFileList"
:drag="drag"
:accept="accept"
:multiple="multiple"
:limit="limit"
:before-upload="before"
:on-success="success"
:on-error="error"
:on-preview="handlePreview"
:on-exceed="handleExceed">
<slot>
<el-button type="primary" :disabled="disabled">点击上传</el-button>
</slot>
<template #tip>
<div v-if="tip" class="el-upload__tip">{{tip}}</div>
</template>
</el-upload>
<span style="display:none!important"><el-input v-model="value"></el-input></span>
</div>
</template>
<script>
import config from "@/config/upload"
export default {
props: {
modelValue: { type: [String, Array], default: "" },
tip: { type: String, default: "" },
action: { type: String, default: "" },
apiObj: { type: Object, default: () => {} },
name: { type: String, default: config.filename },
data: { type: Object, default: () => {} },
accept: { type: String, default: "" },
maxSize: { type: Number, default: config.maxSizeFile },
limit: { type: Number, default: 0 },
autoUpload: { type: Boolean, default: true },
showFileList: { type: Boolean, default: true },
drag: { type: Boolean, default: false },
multiple: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
onSuccess: { type: Function, default: () => { return true } }
},
data() {
return {
value: "",
defaultFileList: []
}
},
watch:{
modelValue(val){
if(Array.isArray(val)){
if (JSON.stringify(val) != JSON.stringify(this.formatArr(this.defaultFileList))) {
this.defaultFileList = val
this.value = val
}
}else{
if (val != this.toStr(this.defaultFileList)) {
this.defaultFileList = this.toArr(val)
this.value = val
}
}
},
defaultFileList: {
handler(val){
this.$emit('update:modelValue', Array.isArray(this.modelValue) ? this.formatArr(val) : this.toStr(val))
this.value = this.toStr(val)
},
deep: true
}
},
mounted() {
this.defaultFileList = Array.isArray(this.modelValue) ? this.modelValue : this.toArr(this.modelValue)
this.value = this.modelValue
},
methods: {
//默认值转换为数组
toArr(str){
if(!str){
return []
}
var _arr = []
var arr = str.split(",")
arr.forEach(item => {
if(item){
var urlArr = item.split('/');
var fileName = urlArr[urlArr.length - 1]
_arr.push({
name: fileName,
url: item
})
}
})
return _arr
},
//数组转换为原始值
toStr(arr){
return arr.map(v => v.url).join(",")
},
//格式化数组值
formatArr(arr){
var _arr = []
arr.forEach(item => {
if(item){
_arr.push({
name: item.name,
url: item.url
})
}
})
return _arr
},
before(file){
const maxSize = file.size / 1024 / 1024 < this.maxSize;
if (!maxSize) {
this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
return false;
}
},
success(res, file){
var os = this.onSuccess(res, file)
if(os!=undefined && os==false){
return false
}
var response = config.parseData(res)
file.name = response.fileName
file.url = response.src
},
error(err){
this.$notify.error({
title: '上传文件未成功',
message: err
})
},
beforeRemove(uploadFile){
return this.$confirm(`是否移除 ${uploadFile.name} ?`, '提示', {
type: 'warning',
}).then(() => {
return true
}).catch(() => {
return false
})
},
handleExceed(){
this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`)
},
handlePreview(uploadFile){
window.open(uploadFile.url)
},
request(param){
var apiObj = config.apiObjFile;
if(this.apiObj){
apiObj = this.apiObj;
}
const data = new FormData();
data.append(param.filename, param.file);
for (const key in param.data) {
data.append(key, param.data[key]);
}
apiObj.post(data, {
onUploadProgress: e => {
const complete = parseInt(((e.loaded / e.total) * 100) | 0, 10)
param.onProgress({percent: complete})
}
}).then(res => {
var response = config.parseData(res);
if(response.code == config.successCode){
param.onSuccess(res)
}else{
param.onError(response.msg || "未知错误")
}
}).catch(err => {
param.onError(err)
})
}
}
}
</script>
<style scoped>
.el-form-item.is-error .sc-upload-file:deep(.el-upload-dragger) {border-color: var(--el-color-danger);}
.sc-upload-file {width: 100%;}
.sc-upload-file:deep(.el-upload-list__item) {transition: none !important;}
</style>
@@ -0,0 +1,282 @@
<template>
<div class="sc-upload" :class="{'sc-upload-round':round}" :style="style">
<div v-if="file && file.status != 'success'" class="sc-upload__uploading">
<div class="sc-upload__progress">
<el-progress :percentage="file.percentage" :text-inside="true" :stroke-width="16"/>
</div>
<el-image class="image" :src="file.tempFile" fit="cover"></el-image>
</div>
<div v-if="file && file.status=='success'" class="sc-upload__img">
<el-image class="image" :src="file.url" :preview-src-list="[file.url]" fit="cover" hide-on-click-modal append-to-body :z-index="9999">
<template #placeholder>
<div class="sc-upload__img-slot">
Loading...
</div>
</template>
</el-image>
<div class="sc-upload__img-actions" v-if="!disabled">
<span class="del" @click="handleRemove()"><el-icon><el-icon-delete /></el-icon></span>
</div>
</div>
<el-upload v-if="!file" class="uploader" ref="uploader"
:auto-upload="cropper?false:autoUpload"
:disabled="disabled"
:show-file-list="showFileList"
:action="action"
:name="name"
:data="data"
:accept="accept"
:limit="1"
:http-request="request"
:on-change="change"
:before-upload="before"
:on-success="success"
:on-error="error"
:on-exceed="handleExceed"
drag>
<slot>
<div class="el-upload--picture-card">
<div class="file-empty">
<el-icon><component :is="icon" /></el-icon>
<h4 v-if="title">{{title}}</h4>
</div>
</div>
</slot>
</el-upload>
<span style="display:none!important"><el-input v-model="value"></el-input></span>
<el-dialog title="剪裁" draggable v-model="cropperDialogVisible" :width="680" @closed="cropperClosed" destroy-on-close>
<sc-cropper :src="cropperFile.tempCropperFile" :compress="compress" :aspectRatio="aspectRatio" ref="cropper"></sc-cropper>
<template #footer>
<el-button @click="cropperDialogVisible=false" > </el-button>
<el-button type="primary" @click="cropperSave"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
import { genFileId } from 'element-plus'
const scCropper = defineAsyncComponent(() => import('@/components/scCropper'))
import config from "@/config/upload"
export default {
props: {
modelValue: { type: String, default: "" },
height: {type: Number, default: 148},
width: {type: Number, default: 148},
title: { type: String, default: "" },
icon: { type: String, default: "el-icon-plus" },
action: { type: String, default: "" },
apiObj: { type: Object, default: () => {} },
name: { type: String, default: config.filename },
data: { type: Object, default: () => {} },
accept: { type: String, default: "image/gif, image/jpeg, image/png" },
maxSize: { type: Number, default: config.maxSizeFile },
limit: { type: Number, default: 1 },
autoUpload: { type: Boolean, default: true },
showFileList: { type: Boolean, default: false },
disabled: { type: Boolean, default: false },
round: { type: Boolean, default: false },
onSuccess: { type: Function, default: () => { return true } },
cropper: { type: Boolean, default: false },
compress: {type: Number, default: 1},
aspectRatio: {type: Number, default: NaN}
},
components: {
scCropper
},
data() {
return {
value: "",
file: null,
style: {
width: this.width + "px",
height: this.height + "px"
},
cropperDialogVisible: false,
cropperFile: null
}
},
watch:{
modelValue(val){
this.value = val
this.newFile(val)
},
value(val){
this.$emit('update:modelValue', val)
}
},
mounted() {
this.value = this.modelValue
this.newFile(this.modelValue)
},
methods: {
newFile(url){
if(url){
this.file = {
status: "success",
url: url
}
}else{
this.file = null
}
},
cropperSave(){
this.$refs.cropper.getCropFile(file => {
file.uid = this.cropperFile.uid
this.cropperFile.raw = file
this.file = this.cropperFile
this.file.tempFile = URL.createObjectURL(this.file.raw)
this.$refs.uploader.submit()
}, this.cropperFile.name, this.cropperFile.type)
this.cropperDialogVisible = false
},
cropperClosed(){
URL.revokeObjectURL(this.cropperFile.tempCropperFile)
delete this.cropperFile.tempCropperFile
},
handleRemove(){
this.clearFiles()
},
clearFiles(){
URL.revokeObjectURL(this.file.tempFile)
this.value = ""
this.file = null
this.$nextTick(()=>{
this.$refs.uploader.clearFiles()
})
},
change(file,files){
if(files.length > 1){
files.splice(0, 1)
}
if(this.cropper && file.status=='ready'){
const acceptIncludes = ["image/gif", "image/jpeg", "image/png"].includes(file.raw.type)
if(!acceptIncludes){
this.$notify.warning({
title: '上传文件警告',
message: '选择的文件非图像类文件'
})
return false
}
this.cropperFile = file
this.cropperFile.tempCropperFile = URL.createObjectURL(file.raw)
this.cropperDialogVisible = true
return false
}
this.file = file
if(file.status=='ready'){
file.tempFile = URL.createObjectURL(file.raw)
}
},
before(file){
const acceptIncludes = this.accept.replace(/\s/g,"").split(",").includes(file.type)
if(!acceptIncludes){
this.$notify.warning({
title: '上传文件警告',
message: '选择的文件非图像类文件'
})
this.clearFiles()
return false
}
const maxSize = file.size / 1024 / 1024 < this.maxSize;
if (!maxSize) {
this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
this.clearFiles()
return false
}
},
handleExceed(files){
const file = files[0]
file.uid = genFileId()
this.$refs.uploader.handleStart(file)
},
success(res, file){
//释放内存删除blob
URL.revokeObjectURL(file.tempFile)
delete file.tempFile
var os = this.onSuccess(res, file)
if(os!=undefined && os==false){
this.$nextTick(() => {
this.file = null
this.value = ""
})
return false
}
var response = config.parseData(res)
file.url = response.src
this.value = file.url
},
error(err){
this.$nextTick(()=>{
this.clearFiles()
})
this.$notify.error({
title: '上传文件未成功',
message: err
})
},
request(param){
var apiObj = config.apiObj;
if(this.apiObj){
apiObj = this.apiObj;
}
const data = new FormData();
data.append(param.filename, param.file);
for (const key in param.data) {
data.append(key, param.data[key]);
}
apiObj.post(data, {
onUploadProgress: e => {
const complete = parseInt(((e.loaded / e.total) * 100) | 0, 10)
param.onProgress({percent: complete})
}
}).then(res => {
var response = config.parseData(res);
if(response.code == config.successCode){
param.onSuccess(res)
}else{
param.onError(response.msg || "未知错误")
}
}).catch(err => {
param.onError(err)
})
}
}
}
</script>
<style scoped>
.el-form-item.is-error .sc-upload .el-upload--picture-card {border-color: var(--el-color-danger);}
.sc-upload .el-upload--picture-card {border-radius: 0;}
.sc-upload .uploader,.sc-upload:deep(.el-upload) {width: 100%;height: 100%;}
.sc-upload__img {width: 100%;height: 100%;position: relative;}
.sc-upload__img .image {width: 100%;height: 100%;}
.sc-upload__img-actions {position: absolute;top:0;right: 0;display: none;}
.sc-upload__img-actions span {display: flex;justify-content: center;align-items: center;width: 25px;height:25px;cursor: pointer;color: #fff;}
.sc-upload__img-actions span i {font-size: 12px;}
.sc-upload__img-actions .del {background: #F56C6C;}
.sc-upload__img:hover .sc-upload__img-actions {display: block;}
.sc-upload__img-slot {display: flex;justify-content: center;align-items: center;width: 100%;height: 100%;font-size: 12px;background-color: var(--el-fill-color-lighter);}
.sc-upload__uploading {width: 100%;height: 100%;position: relative;}
.sc-upload__progress {position: absolute;width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;background-color: var(--el-overlay-color-lighter);z-index: 1;padding:10px;}
.sc-upload__progress .el-progress {width: 100%;}
.sc-upload__uploading .image {width: 100%;height: 100%;}
.sc-upload .file-empty {width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;flex-direction: column;}
.sc-upload .file-empty i {font-size: 28px;}
.sc-upload .file-empty h4 {font-size: 12px;font-weight: normal;color: #8c939d;margin-top: 8px;}
.sc-upload.sc-upload-round {border-radius: 50%;overflow: hidden;}
.sc-upload.sc-upload-round .el-upload--picture-card {border-radius: 50%;}
.sc-upload.sc-upload-round .sc-upload__img-actions {top: auto;left: 0;right: 0;bottom: 0;}
.sc-upload.sc-upload-round .sc-upload__img-actions span {width: 100%;}
</style>
@@ -0,0 +1,249 @@
<template>
<div class="sc-upload-multiple">
<el-upload ref="uploader" list-type="picture-card"
:auto-upload="autoUpload"
:disabled="disabled"
:action="action"
:name="name"
:data="data"
:http-request="request"
v-model:file-list="defaultFileList"
:show-file-list="showFileList"
:accept="accept"
:multiple="multiple"
:limit="limit"
:before-upload="before"
:on-success="success"
:on-error="error"
:on-preview="handlePreview"
:on-exceed="handleExceed">
<slot>
<el-icon><el-icon-plus/></el-icon>
</slot>
<template #tip>
<div v-if="tip" class="el-upload__tip">{{tip}}</div>
</template>
<template #file="{ file }">
<div class="sc-upload-list-item">
<el-image class="el-upload-list__item-thumbnail" :src="file.url" fit="cover" :preview-src-list="preview" :initial-index="preview.findIndex(n=>n==file.url)" hide-on-click-modal append-to-body :z-index="9999">
<template #placeholder>
<div class="sc-upload-multiple-image-slot">
Loading...
</div>
</template>
</el-image>
<div v-if="!disabled && file.status=='success'" class="sc-upload__item-actions">
<span class="del" @click="handleRemove(file)"><el-icon><el-icon-delete /></el-icon></span>
</div>
<div v-if="file.status=='ready' || file.status=='uploading'" class="sc-upload__item-progress">
<el-progress :percentage="file.percentage" :text-inside="true" :stroke-width="16"/>
</div>
</div>
</template>
</el-upload>
<span style="display:none!important"><el-input v-model="value"></el-input></span>
</div>
</template>
<script>
import config from "@/config/upload"
import Sortable from 'sortablejs'
export default {
props: {
modelValue: { type: [String, Array], default: "" },
tip: { type: String, default: "" },
action: { type: String, default: "" },
apiObj: { type: Object, default: () => {} },
name: { type: String, default: config.filename },
data: { type: Object, default: () => {} },
accept: { type: String, default: "image/gif, image/jpeg, image/png" },
maxSize: { type: Number, default: config.maxSizeFile },
limit: { type: Number, default: 0 },
autoUpload: { type: Boolean, default: true },
showFileList: { type: Boolean, default: true },
multiple: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
draggable: { type: Boolean, default: false },
onSuccess: { type: Function, default: () => { return true } }
},
data(){
return {
value: "",
defaultFileList: []
}
},
watch:{
modelValue(val){
if(Array.isArray(val)){
if (JSON.stringify(val) != JSON.stringify(this.formatArr(this.defaultFileList))) {
this.defaultFileList = val
this.value = val
}
}else{
if (val != this.toStr(this.defaultFileList)) {
this.defaultFileList = this.toArr(val)
this.value = val
}
}
},
defaultFileList: {
handler(val){
this.$emit('update:modelValue', Array.isArray(this.modelValue) ? this.formatArr(val) : this.toStr(val))
this.value = this.toStr(val)
},
deep: true
}
},
computed: {
preview(){
return this.defaultFileList.map(v => v.url)
}
},
mounted() {
this.defaultFileList = Array.isArray(this.modelValue) ? this.modelValue : this.toArr(this.modelValue)
this.value = this.modelValue
if(!this.disabled && this.draggable){
this.rowDrop()
}
},
methods: {
//默认值转换为数组
toArr(str){
var _arr = [];
var arr = str.split(",");
arr.forEach(item => {
if(item){
var urlArr = item.split('/');
var fileName = urlArr[urlArr.length - 1]
_arr.push({
name: fileName,
url: item
})
}
})
return _arr;
},
//数组转换为原始值
toStr(arr){
return arr.map(v => v.url).join(",")
},
//格式化数组值
formatArr(arr){
var _arr = []
arr.forEach(item => {
if(item){
_arr.push({
name: item.name,
url: item.url
})
}
})
return _arr
},
//拖拽
rowDrop(){
const _this = this
const itemBox = this.$refs.uploader.$el.querySelector('.el-upload-list')
Sortable.create(itemBox, {
handle: ".el-upload-list__item",
animation: 200,
ghostClass: "ghost",
onEnd({ newIndex, oldIndex }) {
const tableData = _this.defaultFileList
const currRow = tableData.splice(oldIndex, 1)[0]
tableData.splice(newIndex, 0, currRow)
}
})
},
before(file){
if(!['image/jpeg','image/png','image/gif'].includes(file.type)){
this.$message.warning(`选择的文件类型 ${file.type} 非图像类文件`);
return false;
}
const maxSize = file.size / 1024 / 1024 < this.maxSize;
if (!maxSize) {
this.$message.warning(`上传文件大小不能超过 ${this.maxSize}MB!`);
return false;
}
},
success(res, file){
var os = this.onSuccess(res, file)
if(os!=undefined && os==false){
return false
}
var response = config.parseData(res)
file.name = response.fileName
file.url = response.src
},
error(err){
this.$notify.error({
title: '上传文件未成功',
message: err
})
},
beforeRemove(uploadFile){
return this.$confirm(`是否移除 ${uploadFile.name} ?`, '提示', {
type: 'warning',
}).then(() => {
return true
}).catch(() => {
return false
})
},
handleRemove(file){
this.$refs.uploader.handleRemove(file)
//this.defaultFileList.splice(this.defaultFileList.findIndex(item => item.uid===file.uid), 1)
},
handleExceed(){
this.$message.warning(`当前设置最多上传 ${this.limit} 个文件,请移除后上传!`)
},
handlePreview(uploadFile){
window.open(uploadFile.url)
},
request(param){
var apiObj = config.apiObj;
if(this.apiObj){
apiObj = this.apiObj;
}
const data = new FormData();
data.append(param.filename, param.file);
for (const key in param.data) {
data.append(key, param.data[key]);
}
apiObj.post(data, {
onUploadProgress: e => {
const complete = parseInt(((e.loaded / e.total) * 100) | 0, 10)
param.onProgress({percent: complete})
}
}).then(res => {
var response = config.parseData(res);
if(response.code == config.successCode){
param.onSuccess(res)
}else{
param.onError(response.msg || "未知错误")
}
}).catch(err => {
param.onError(err)
})
}
}
}
</script>
<style scoped>
.el-form-item.is-error .sc-upload-multiple:deep(.el-upload--picture-card) {border-color: var(--el-color-danger);}
:deep(.el-upload-list__item) {transition:none;border-radius: 0;}
.sc-upload-multiple:deep(.el-upload-list__item.el-list-leave-active) {position: static!important;}
.sc-upload-multiple:deep(.el-upload--picture-card) {border-radius: 0;}
.sc-upload-list-item {width: 100%;height: 100%;position: relative;}
.sc-upload-multiple .el-image {display: block;}
.sc-upload-multiple .el-image:deep(img) {-webkit-user-drag: none;}
.sc-upload-multiple-image-slot {display: flex;justify-content: center;align-items: center;width: 100%;height: 100%;font-size: 12px;}
.sc-upload-multiple .el-upload-list__item:hover .sc-upload__item-actions{display: block;}
.sc-upload__item-actions {position: absolute;top:0;right: 0;display: none;}
.sc-upload__item-actions span {display: flex;justify-content: center;align-items: center;;width: 25px;height:25px;cursor: pointer;color: #fff;}
.sc-upload__item-actions span i {font-size: 12px;}
.sc-upload__item-actions .del {background: #F56C6C;}
.sc-upload__item-progress {position: absolute;width: 100%;height: 100%;top: 0;left: 0;background-color: var(--el-overlay-color-lighter);}
</style>
@@ -0,0 +1,265 @@
<!--
* @Descripttion: 表格选择器组件
* @version: 1.2
* @Author: sakuya
* @Date: 2021年6月10日10:04:07
* @LastEditors: sakuya
* @LastEditTime: 2022年2月28日09:39:03
-->
<template>
<el-select ref="select" v-model="defaultValue" clearable :multiple="multiple" filterable :placeholder="placeholder" :disabled="disabled" :filter-method="filterMethod" @remove-tag="removeTag" @visible-change="visibleChange" @clear="clear">
<template #empty>
<div class="sc-table-select__table" :style="{width: tableWidth+'px'}" v-loading="loading">
<el-container>
<el-header>
<el-form :inline="true" :model="formData">
<el-form-item>
<el-input v-model="formData.name" placeholder="姓名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="formSubmit">查询</el-button>
</el-form-item>
</el-form>
</el-header>
<el-container>
<el-aside width="210px">
<div style="height:300px">
<el-tree :props="departmentProps" :data="department" node-key="id" @node-click="groupClick"/>
</div>
</el-aside>
<el-main>
<el-table ref="table" :data="tableData" :height="245" :highlight-current-row="!multiple" @row-click="click" @select="select" @select-all="selectAll">
<el-table-column v-if="multiple" type="selection" width="45"></el-table-column>
<el-table-column prop="uid" label="ID" width="80"></el-table-column>
<el-table-column prop="username" label="用户名" width="150"></el-table-column>
<el-table-column prop="nickname" label="姓名" width="150"></el-table-column>
</el-table>
<div class="sc-table-select__page">
<el-pagination small background layout="prev, pager, next" :total="total" :page-size="pageSize" :page-sizes="[100, 200, 300, 400]" v-model:currentPage="currentPage" @current-change="reload"></el-pagination>
</div>
</el-main>
</el-container>
</el-container>
</div>
</template>
</el-select>
</template>
<script>
import config from "@/config/tableSelect";
export default {
props: {
modelValue: null,
params: { type: Object, default: () => {} },
placeholder: { type: String, default: "请选择" },
multiple: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
tableWidth: {type: Number, default: 620},
mode: { type: String, default: "popover" },
},
data() {
return {
apiObj: this.$API.user.list,
props: {
label: 'nickname',
value: 'uid',
keyword: "name"
},
//所需数据选项
department: [],
departmentProps: {
value: "id",
label: "title"
},
loading: false,
keyword: null,
defaultValue: [],
tableData: [],
pageSize: config.pageSize,
total: 0,
currentPage: 1,
defaultProps: {
label: config.props.label,
value: config.props.value,
page: config.request.page,
pageSize: config.request.pageSize,
keyword: config.request.keyword
},
formData: {}
}
},
computed: {
},
watch: {
modelValue:{
handler(){
this.defaultValue = this.modelValue
this.autoCurrentLabel()
},
deep: true
}
},
mounted() {
this.defaultProps = Object.assign(this.defaultProps, this.props);
this.defaultValue = this.modelValue
this.autoCurrentLabel()
this.getDepartment();
},
methods: {
async getDepartment(){
var res = await this.$API.user.department.list.get();
this.department = res.data;
},
groupClick(data){
this.formData.department_id = data.id;
this.getData();
},
//表格显示隐藏回调
visibleChange(visible){
if(visible){
this.currentPage = 1
this.keyword = null
this.formData = {}
this.getData()
}else{
this.autoCurrentLabel()
}
},
//获取表格数据
async getData(){
this.loading = true;
var reqData = {
[this.defaultProps.page]: this.currentPage,
[this.defaultProps.pageSize]: this.pageSize,
[this.defaultProps.keyword]: this.keyword
}
Object.assign(reqData, this.params, this.formData)
var res = await this.apiObj.get(reqData);
var parseData = config.parseData(res)
this.tableData = parseData.rows;
this.total = parseData.total;
this.loading = false;
//表格默认赋值
this.$nextTick(() => {
if(this.multiple){
this.defaultValue.forEach(row => {
var setrow = this.tableData.filter(item => item[this.defaultProps.value]===row[this.defaultProps.value] )
if(setrow.length > 0){
this.$refs.table.toggleRowSelection(setrow[0], true);
}
})
}else{
var setrow = this.tableData.filter(item => item[this.defaultProps.value]===this.defaultValue[this.defaultProps.value] )
this.$refs.table.setCurrentRow(setrow[0]);
}
this.$refs.table.$el.querySelector('.el-table__body-wrapper').scrollTop = 0
})
},
//插糟表单提交
formSubmit(){
this.currentPage = 1
this.keyword = null
this.getData()
},
//分页刷新表格
reload(){
this.getData()
},
//自动模拟options赋值
autoCurrentLabel(){
this.$nextTick(() => {
if(this.multiple){
this.$refs.select.selected.forEach(item => {
item.currentLabel = item.value[this.defaultProps.label]
})
}else{
this.$refs.select.selectedLabel = this.defaultValue[this.defaultProps.label]
}
})
},
//表格勾选事件
select(rows, row){
var isSelect = rows.length && rows.indexOf(row) !== -1
if(isSelect){
this.defaultValue.push(row)
}else{
this.defaultValue.splice(this.defaultValue.findIndex(item => item[this.defaultProps.value] == row[this.defaultProps.value]), 1)
}
this.autoCurrentLabel()
this.$emit('update:modelValue', this.defaultValue);
this.$emit('change', this.defaultValue);
},
//表格全选事件
selectAll(rows){
var isAllSelect = rows.length > 0
if(isAllSelect){
rows.forEach(row => {
var isHas = this.defaultValue.find(item => item[this.defaultProps.value] == row[this.defaultProps.value])
if(!isHas){
this.defaultValue.push(row)
}
})
}else{
this.tableData.forEach(row => {
var isHas = this.defaultValue.find(item => item[this.defaultProps.value] == row[this.defaultProps.value])
if(isHas){
this.defaultValue.splice(this.defaultValue.findIndex(item => item[this.defaultProps.value] == row[this.defaultProps.value]), 1)
}
})
}
this.autoCurrentLabel()
this.$emit('update:modelValue', this.defaultValue);
this.$emit('change', this.defaultValue);
},
click(row){
if(this.multiple){
//处理多选点击行
}else{
this.defaultValue = row
this.$refs.select.blur()
this.autoCurrentLabel()
this.$emit('update:modelValue', this.defaultValue);
this.$emit('change', this.defaultValue);
}
},
//tags删除后回调
removeTag(tag){
var row = this.findRowByKey(tag[this.defaultProps.value])
this.$refs.table.toggleRowSelection(row, false);
this.$emit('update:modelValue', this.defaultValue);
},
//清空后的回调
clear(){
this.$emit('update:modelValue', this.defaultValue);
},
// 关键值查询表格数据行
findRowByKey (value) {
return this.tableData.find(item => item[this.defaultProps.value] === value)
},
filterMethod(keyword){
if(!keyword){
this.keyword = null;
return false;
}
this.keyword = keyword;
this.getData()
},
// 触发select隐藏
blur(){
this.$refs.select.blur();
},
// 触发select显示
focus(){
this.$refs.select.focus();
}
}
}
</script>
<style scoped>
.sc-table-select__table {padding:12px;}
.sc-table-select__page {padding-top: 12px;}
</style>
@@ -0,0 +1,84 @@
<!--
* @Descripttion: xgplayer二次封装
* @version: 1.1
* @Author: sakuya
* @Date: 2021年11月29日12:10:06
* @LastEditors: sakuya
* @LastEditTime: 2022年5月30日21:02:50
-->
<template>
<div class="sc-video" ref="scVideo"></div>
</template>
<script>
import Player from 'xgplayer'
import HlsPlayer from 'xgplayer-hls'
export default {
props: {
src: { type: String, required: true, default: "" },
autoplay: { type: Boolean, default: false },
controls: { type: Boolean, default: true },
loop: { type: Boolean, default: false },
isLive: { type: Boolean, default: false },
options: { type: Object, default: () => {} }
},
data() {
return {
player: null
}
},
watch:{
src(val){
if(this.player.hasStart){
this.player.src = val
}else{
this.player.start(val)
}
}
},
mounted() {
if(this.isLive){
this.initHls()
}else{
this.init()
}
},
methods: {
init(){
this.player = new Player({
el: this.$refs.scVideo,
url: this.src,
autoplay: this.autoplay,
loop: this.loop,
controls: this.controls,
fluid: true,
lang: 'zh-cn',
...this.options
})
},
initHls(){
this.player = new HlsPlayer({
el: this.$refs.scVideo,
url: this.src,
autoplay: this.autoplay,
loop: this.loop,
controls: this.controls,
fluid: true,
isLive: true,
ignores: ['time','progress'],
lang: 'zh-cn',
...this.options
})
}
}
}
</script>
<style scoped>
.sc-video:deep(.danmu) > * {color: #fff;font-size:20px;font-weight:bold;text-shadow:1px 1px 0 #000,-1px -1px 0 #000,-1px 1px 0 #000,1px -1px 0 #000;}
.sc-video:deep(.xgplayer-controls) {background-image: linear-gradient(180deg, transparent, rgba(0,0,0,0.3));}
.sc-video:deep(.xgplayer-progress-tip) {border:0;color: #fff;background: rgba(0,0,0,.5);line-height: 25px;padding: 0 10px;border-radius: 25px;}
.sc-video:deep(.xgplayer-enter-spinner) {width: 50px;height: 50px;}
</style>
@@ -0,0 +1,66 @@
<!--
* @Descripttion: 局部水印组件
* @version: 1.1
* @Author: sakuya
* @Date: 2021年12月18日12:16:16
* @LastEditors: sakuya
* @LastEditTime: 2022年1月5日09:52:59
-->
<template>
<div class="sc-water-mark" ref="scWaterMark">
<slot></slot>
</div>
</template>
<script>
export default {
props: {
text: { type: String, required: true, default: "" },
subtext: { type: String, default: "" },
color: { type: String, default: "rgba(128,128,128,0.2)" }
},
data() {
return {
}
},
mounted() {
this.create()
},
methods: {
create(){
this.clear()
//创建画板
var canvas = document.createElement('canvas')
canvas.width = 150
canvas.height = 150
canvas.style.display = 'none'
//绘制文字
var text = canvas.getContext('2d')
text.rotate(-45 * Math.PI / 180)
text.translate(-75, 25)
text.fillStyle = this.color
text.font = "bold 20px SimHei"
text.textAlign = "center"
text.fillText(this.text, canvas.width / 2, canvas.height / 2)
text.font = "14px Microsoft YaHei"
text.fillText(this.subtext, canvas.width / 2, canvas.height / 2 + 20)
//创建水印容器
var watermark = document.createElement('div')
watermark.setAttribute('class', 'watermark')
const styleStr = `position:absolute;top:0;left:0;right:0;bottom:0;z-index:99;pointer-events:none;background-repeat:repeat;background-image:url('${canvas.toDataURL("image/png")}');`
watermark.setAttribute('style', styleStr);
this.$refs.scWaterMark.appendChild(watermark)
},
clear(){
var wmDom = this.$refs.scWaterMark.querySelector('.watermark')
wmDom && wmDom.remove()
}
}
}
</script>
<style scoped>
.sc-water-mark {position: relative;display: inherit;width: 100%;height: 100%;}
</style>
@@ -0,0 +1,154 @@
<!--
* @Descripttion: 仿钉钉流程设计器
* @version: 1.3
* @Author: sakuya
* @Date: 2021年9月14日08:38:35
* @LastEditors: sakuya
* @LastEditTime: 2022年5月14日19:43:46
-->
<template>
<div class="sc-workflow-design">
<div class="box-scale">
<node-wrap v-if="nodeConfig" v-model="nodeConfig"></node-wrap>
<div class="end-node">
<div class="end-node-circle"></div>
<div class="end-node-text">流程结束</div>
</div>
</div>
<use-select v-if="selectVisible" ref="useselect" @closed="selectVisible=false"></use-select>
</div>
</template>
<script>
import nodeWrap from './nodeWrap'
import useSelect from './select'
export default {
provide(){
return {
select: this.selectHandle
}
},
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
nodeWrap,
useSelect
},
data() {
return {
nodeConfig: this.modelValue,
selectVisible: false
}
},
watch:{
modelValue(val){
this.nodeConfig = val
},
nodeConfig(val){
this.$emit("update:modelValue", val)
}
},
mounted() {
},
methods: {
selectHandle(type, data){
this.selectVisible = true
this.$nextTick(() => {
this.$refs.useselect.open(type, data)
})
}
}
}
</script>
<style lang="scss">
.sc-workflow-design {width: 100%;}
.sc-workflow-design .box-scale {display: inline-block;position: relative;width: 100%;padding: 54.5px 0px;align-items: flex-start;justify-content: center;flex-wrap: wrap;min-width: min-content;}
.sc-workflow-design {
.node-wrap {display: inline-flex;width: 100%;flex-flow: column wrap;justify-content: flex-start;align-items: center;padding: 0px 50px;position: relative;z-index: 1;}
.node-wrap-box {display: inline-flex;flex-direction: column;position: relative;width: 220px;min-height: 72px;flex-shrink: 0;background: rgb(255, 255, 255);border-radius: 4px;cursor: pointer;box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);}
.node-wrap-box::before {content: "";position: absolute;top: -12px;left: 50%;transform: translateX(-50%);width: 0px;border-style: solid;border-width: 8px 6px 4px;border-color: rgb(202, 202, 202) transparent transparent;background: #f6f8f9;}
.node-wrap-box.start-node:before {content: none}
.node-wrap-box .title {height:24px;line-height: 24px;color: #fff;padding-left: 16px;padding-right: 30px;border-radius: 4px 4px 0 0;position: relative;display: flex;align-items: center;}
.node-wrap-box .title .icon {margin-right: 5px;}
.node-wrap-box .title .close {font-size: 15px;position: absolute;top:50%;transform: translateY(-50%);right:10px;display: none;}
.node-wrap-box .content {position: relative;padding: 15px;}
.node-wrap-box .content .placeholder {color: #999;}
.node-wrap-box:hover .close {display: block;}
.add-node-btn-box {width: 240px;display: inline-flex;flex-shrink: 0;position: relative;z-index: 1;}
.add-node-btn-box:before {content: "";position: absolute;top: 0px;left: 0px;right: 0px;bottom: 0px;z-index: -1;margin: auto;width: 2px;height: 100%;background-color: rgb(202, 202, 202);}
.add-node-btn {user-select: none;width: 240px;padding: 20px 0px 32px;display: flex;justify-content: center;flex-shrink: 0;flex-grow: 1;}
.add-node-btn span {}
.add-branch {justify-content: center;padding: 0px 10px;position: absolute;top: -16px;left: 50%;transform: translateX(-50%);transform-origin: center center;z-index: 1;display: inline-flex;align-items: center;}
.branch-wrap {display: inline-flex;width: 100%;}
.branch-box-wrap {display: flex;flex-flow: column wrap;align-items: center;min-height: 270px;width: 100%;flex-shrink: 0;}
.col-box {display: inline-flex;flex-direction: column;align-items: center;position: relative;background: #f6f8f9;}
.branch-box {display: flex;overflow: visible;min-height: 180px;height: auto;border-bottom: 2px solid #ccc;border-top: 2px solid #ccc;position: relative;margin-top: 15px;}
.branch-box .col-box::before {content: "";position: absolute;top: 0px;left: 0px;right: 0px;bottom: 0px;z-index: 0;margin: auto;width: 2px;height: 100%;background-color: rgb(202, 202, 202);}
.condition-node {display: inline-flex;flex-direction: column;min-height: 220px;}
.condition-node-box {padding-top: 30px;padding-right: 50px;padding-left: 50px;justify-content: center;align-items: center;flex-grow: 1;position: relative;display: inline-flex;flex-direction: column;}
.condition-node-box::before {content: "";position: absolute;top: 0px;left: 0px;right: 0px;bottom: 0px;margin: auto;width: 2px;height: 100%;background-color: rgb(202, 202, 202);}
.auto-judge {position: relative;width: 220px;min-height: 72px;background: rgb(255, 255, 255);border-radius: 4px;padding: 15px 15px;cursor: pointer;box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);}
.auto-judge::before {content: "";position: absolute;top: -12px;left: 50%;transform: translateX(-50%);width: 0px;border-style: solid;border-width: 8px 6px 4px;border-color: rgb(202, 202, 202) transparent transparent;background: rgb(245, 245, 247);}
.auto-judge .title {line-height: 16px;}
.auto-judge .title .node-title {color: #15BC83;}
.auto-judge .title .close {font-size: 15px;position: absolute;top:15px;right:15px;color: #999;display: none;}
.auto-judge .title .priority-title {position: absolute;top:15px;right:15px;color: #999;}
.auto-judge .content {position: relative;padding-top: 15px;}
.auto-judge .content .placeholder {color: #999;}
.auto-judge:hover {
.close {display: block;}
.priority-title {display: none;}
}
.top-left-cover-line, .top-right-cover-line {position: absolute;height: 3px;width: 50%;background-color: #f6f8f9;top: -2px;}
.bottom-left-cover-line, .bottom-right-cover-line {position: absolute;height: 3px;width: 50%;background-color: #f6f8f9;bottom: -2px;}
.top-left-cover-line {left: -1px;}
.top-right-cover-line {right: -1px;}
.bottom-left-cover-line {left: -1px;}
.bottom-right-cover-line {right: -1px;}
.end-node {border-radius: 50%;font-size: 14px;color: rgba(25,31,37,.4);text-align: left;}
.end-node-circle {width: 10px;height: 10px;margin: auto;border-radius: 50%;background: #dbdcdc;}
.end-node-text {margin-top: 5px;text-align: center;}
.auto-judge:hover {
.sort-left {display: flex;}
.sort-right {display: flex;}
}
.auto-judge .sort-left {position: absolute;top: 0;bottom: 0;z-index: 1;left: 0;display: none;justify-content: center;align-items: center;flex-direction: column;}
.auto-judge .sort-right {position: absolute;top: 0;bottom: 0;z-index: 1;right: 0;display: none;justify-content: center;align-items: center;flex-direction: column;}
.auto-judge .sort-left:hover, .auto-judge .sort-right:hover {background: #eee;}
.auto-judge:after {pointer-events: none;content: "";position: absolute;top:0;bottom:0;left:0;right:0;z-index: 2;border-radius: 4px;transition: all .1s;}
.auto-judge:hover:after {border: 1px solid #3296fa;box-shadow: 0 0 6px 0 rgba(50,150,250,.3);}
.node-wrap-box:after {pointer-events: none;content: "";position: absolute;top:0;bottom:0;left:0;right:0;z-index: 2;border-radius: 4px;transition: all .1s;}
.node-wrap-box:hover:after {border: 1px solid #3296fa;box-shadow: 0 0 6px 0 rgba(50,150,250,.3);}
}
.tags-list {margin-top: 15px;width: 100%;}
.add-node-popover-body {}
.add-node-popover-body li {display: inline-block;width: 80px;text-align: center;padding:10px 0;}
.add-node-popover-body li i {border: 1px solid var(--el-border-color-light);width:40px;height:40px;border-radius: 50%;text-align: center;line-height: 38px;font-size: 18px;cursor: pointer;}
.add-node-popover-body li i:hover {border: 1px solid #3296fa;background: #3296fa;color: #fff!important;}
.add-node-popover-body li p {font-size: 12px;margin-top: 5px;}
.node-wrap-drawer__title {padding-right:40px;}
.node-wrap-drawer__title label {cursor: pointer;}
.node-wrap-drawer__title label:hover {border-bottom: 1px dashed #409eff;}
.node-wrap-drawer__title .node-wrap-drawer__title-edit {color: #409eff;margin-left: 10px;vertical-align: middle;}
.dark .sc-workflow-design {
.node-wrap-box,.auto-judge {background: #2b2b2b;}
.col-box {background: var(--el-bg-color);}
.top-left-cover-line,
.top-right-cover-line,
.bottom-left-cover-line,
.bottom-right-cover-line {background-color: var(--el-bg-color);}
.node-wrap-box::before,.auto-judge::before {background-color: var(--el-bg-color);}
.branch-box .add-branch {background: var(--el-bg-color);}
.end-node .end-node-text {color: #d0d0d0;}
.auto-judge .sort-left:hover, .auto-judge .sort-right:hover {background: var(--el-bg-color);}
}
</style>
@@ -0,0 +1,57 @@
<template>
<promoter v-if="nodeConfig.type==0" v-model="nodeConfig"></promoter>
<approver v-if="nodeConfig.type==1" v-model="nodeConfig"></approver>
<send v-if="nodeConfig.type==2" v-model="nodeConfig"></send>
<branch v-if="nodeConfig.type==4" v-model="nodeConfig">
<template v-slot="slot">
<node-wrap v-if="slot.node" v-model="slot.node.childNode"></node-wrap>
</template>
</branch>
<node-wrap v-if="nodeConfig.childNode" v-model="nodeConfig.childNode"></node-wrap>
</template>
<script>
import approver from './nodes/approver'
import promoter from './nodes/promoter'
import branch from './nodes/branch'
import send from './nodes/send'
export default {
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
approver,
promoter,
branch,
send
},
data() {
return {
nodeConfig: {},
}
},
watch:{
modelValue(val){
this.nodeConfig = val
},
nodeConfig(val){
this.$emit("update:modelValue", val)
}
},
mounted() {
this.nodeConfig = this.modelValue
},
methods: {
}
}
</script>
<style>
</style>
@@ -0,0 +1,102 @@
<template>
<div class="add-node-btn-box">
<div class="add-node-btn">
<el-popover placement="right-start" :width="270" trigger="click" :hide-after="0" :show-after="0">
<template #reference>
<el-button type="primary" icon="el-icon-plus" circle></el-button>
</template>
<div class="add-node-popover-body">
<ul>
<li>
<el-icon style="color: #ff943e;" @click="addType(1)"><el-icon-user-filled /></el-icon>
<p>审批节点</p>
</li>
<li>
<el-icon style="color: #3296fa;" @click="addType(2)"><el-icon-promotion /></el-icon>
<p>抄送节点</p>
</li>
<li>
<el-icon style="color: #15BC83;" @click="addType(4)"><el-icon-share /></el-icon>
<p>条件分支</p>
</li>
</ul>
</div>
</el-popover>
</div>
</div>
</template>
<script>
export default {
props: {
modelValue: { type: Object, default: () => {} }
},
data() {
return {
}
},
mounted() {
},
methods: {
addType(type){
var node = {}
if (type == 1) {
node = {
nodeName: "审核人",
type: 1, //节点类型
setType: 1, //审核人类型
nodeUserList: [], //审核人成员
nodeRoleList: [], //审核角色
examineLevel: 1, //指定主管层级
directorLevel: 1, //自定义连续主管审批层级
selectMode: 1, //发起人自选类型
termAuto: false, //审批期限超时自动审批
term: 0, //审批期限
termMode: 1, //审批期限超时后执行类型
examineMode: 1, //多人审批时审批方式
directorMode: 0, //连续主管审批方式
childNode: this.modelValue
}
}else if(type == 2){
node = {
nodeName: "抄送人",
type: 2,
userSelectFlag: true,
nodeUserList: [],
childNode: this.modelValue
}
}else if(type == 4){
node = {
nodeName: "条件路由",
type: 4,
conditionNodes: [
{
nodeName: "条件1",
type: 3,
priorityLevel: 1,
conditionMode: 1,
conditionList: []
},
{
nodeName: "条件2",
type: 3,
priorityLevel: 2,
conditionMode: 1,
conditionList: []
}
],
childNode: this.modelValue
}
}
this.$emit("update:modelValue", node)
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,193 @@
<template>
<div class="node-wrap">
<div class="node-wrap-box" @click="show">
<div class="title" style="background: #ff943e;">
<el-icon class="icon"><el-icon-user-filled /></el-icon>
<span>{{ nodeConfig.nodeName }}</span>
<el-icon class="close" @click.stop="delNode()"><el-icon-close /></el-icon>
</div>
<div class="content">
<span v-if="toText(nodeConfig)">{{ toText(nodeConfig) }}</span>
<span v-else class="placeholder">请选择</span>
</div>
</div>
<add-node v-model="nodeConfig.childNode"></add-node>
<el-drawer title="审批人设置" v-model="drawer" destroy-on-close append-to-body :size="500">
<template #header>
<div class="node-wrap-drawer__title">
<label @click="editTitle" v-if="!isEditTitle">{{form.nodeName}}<el-icon class="node-wrap-drawer__title-edit"><el-icon-edit /></el-icon></label>
<el-input v-if="isEditTitle" ref="nodeTitle" v-model="form.nodeName" clearable @blur="saveTitle" @keyup.enter="saveTitle"></el-input>
</div>
</template>
<el-container>
<el-main style="padding:0 20px 20px 20px">
<el-form label-position="top">
<el-form-item label="审批人员类型">
<el-select v-model="form.setType">
<el-option :value="1" label="指定成员"></el-option>
<el-option :value="2" label="主管"></el-option>
<el-option :value="3" label="角色"></el-option>
<el-option :value="4" label="发起人自选"></el-option>
<el-option :value="5" label="发起人自己"></el-option>
<el-option :value="7" label="连续多级主管"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="form.setType==1" label="选择成员">
<el-button type="primary" icon="el-icon-plus" round @click="selectHandle(1, form.nodeUserList)">选择人员</el-button>
<div class="tags-list">
<el-tag v-for="(user, index) in form.nodeUserList" :key="user.id" closable @close="delUser(index)">{{user.name}}</el-tag>
</div>
</el-form-item>
<el-form-item v-if="form.setType==2" label="指定主管">
发起人的第 <el-input-number v-model="form.examineLevel" :min="1"/> 级主管
</el-form-item>
<el-form-item v-if="form.setType==3" label="选择角色">
<el-button type="primary" icon="el-icon-plus" round @click="selectHandle(2, form.nodeRoleList)">选择角色</el-button>
<div class="tags-list">
<el-tag v-for="(role, index) in form.nodeRoleList" :key="role.id" type="info" closable @close="delRole(index)">{{role.name}}</el-tag>
</div>
</el-form-item>
<el-form-item v-if="form.setType==4" label="发起人自选">
<el-radio-group v-model="form.selectMode">
<el-radio :label="1">自选一个人</el-radio>
<el-radio :label="2">自选多个人</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="form.setType==7" label="连续主管审批终点">
<el-radio-group v-model="form.directorMode">
<el-radio :label="0">直到最上层主管</el-radio>
<el-radio :label="1">自定义审批终点</el-radio>
</el-radio-group>
<p v-if="form.directorMode==1">直到发起人的第 <el-input-number v-model="form.directorLevel" :min="1"/> 级主管</p>
</el-form-item>
<el-divider></el-divider>
<el-form-item label="">
<el-checkbox v-model="form.termAuto" label="超时自动审批"></el-checkbox>
</el-form-item>
<template v-if="form.termAuto">
<el-form-item label="审批期限(为 0 则不生效)">
<el-input-number v-model="form.term" :min="0"/> 小时
</el-form-item>
<el-form-item label="审批期限超时后执行">
<el-radio-group v-model="form.termMode">
<el-radio :label="0">自动通过</el-radio>
<el-radio :label="1">自动拒绝</el-radio>
</el-radio-group>
</el-form-item>
</template>
<el-divider></el-divider>
<el-form-item label="多人审批时审批方式">
<el-radio-group v-model="form.examineMode">
<p style="width: 100%;"><el-radio :label="1">按顺序依次审批</el-radio></p>
<p style="width: 100%;"><el-radio :label="2">会签 (可同时审批每个人必须审批通过)</el-radio></p>
<p style="width: 100%;"><el-radio :label="3">或签 (有一人审批通过即可)</el-radio></p>
</el-radio-group>
</el-form-item>
</el-form>
</el-main>
<el-footer>
<el-button type="primary" @click="save">保存</el-button>
<el-button @click="drawer=false">取消</el-button>
</el-footer>
</el-container>
</el-drawer>
</div>
</template>
<script>
import addNode from './addNode'
export default {
inject: ['select'],
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
addNode
},
data() {
return {
nodeConfig: {},
drawer: false,
isEditTitle: false,
form: {}
}
},
watch:{
modelValue(){
this.nodeConfig = this.modelValue
}
},
mounted() {
this.nodeConfig = this.modelValue
},
methods: {
show(){
this.form = {}
this.form = JSON.parse(JSON.stringify(this.nodeConfig))
this.drawer = true
},
editTitle(){
this.isEditTitle = true
this.$nextTick(()=>{
this.$refs.nodeTitle.focus()
})
},
saveTitle(){
this.isEditTitle = false
},
save(){
this.$emit("update:modelValue", this.form)
this.drawer = false
},
delNode(){
this.$emit("update:modelValue", this.nodeConfig.childNode)
},
delUser(index){
this.form.nodeUserList.splice(index, 1)
},
delRole(index){
this.form.nodeRoleList.splice(index, 1)
},
selectHandle(type, data){
this.select(type, data)
},
toText(nodeConfig){
if(nodeConfig.setType == 1){
if (nodeConfig.nodeUserList && nodeConfig.nodeUserList.length>0) {
const users = nodeConfig.nodeUserList.map(item=>item.name).join("、")
return users
}else{
return false
}
}else if (nodeConfig.setType == 2) {
return nodeConfig.examineLevel == 1 ? '直接主管' : `发起人的第${nodeConfig.examineLevel}级主管`
}else if (nodeConfig.setType == 3) {
if (nodeConfig.nodeRoleList && nodeConfig.nodeRoleList.length>0) {
const roles = nodeConfig.nodeRoleList.map(item=>item.name).join("、")
return '角色-' + roles
}else{
return false
}
}else if (nodeConfig.setType == 4) {
return "发起人自选"
}else if (nodeConfig.setType == 5) {
return "发起人自己"
}else if (nodeConfig.setType == 7) {
return "连续多级主管"
}
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,222 @@
<template>
<div class="branch-wrap">
<div class="branch-box-wrap">
<div class="branch-box">
<el-button class="add-branch" type="success" plain round @click="addTerm">添加条件</el-button>
<div class="col-box" v-for="(item,index) in nodeConfig.conditionNodes" :key="index">
<div class="condition-node">
<div class="condition-node-box">
<div class="auto-judge" @click="show(index)">
<div class="sort-left" v-if="index!=0" @click.stop="arrTransfer(index,-1)">
<el-icon><el-icon-arrow-left /></el-icon>
</div>
<div class="title">
<span class="node-title">{{ item.nodeName }}</span>
<span class="priority-title">优先级{{item.priorityLevel}}</span>
<el-icon class="close" @click.stop="delTerm(index)"><el-icon-close /></el-icon>
</div>
<div class="content">
<span v-if="toText(nodeConfig, index)">{{ toText(nodeConfig, index) }}</span>
<span v-else class="placeholder">请设置条件</span>
</div>
<div class="sort-right" v-if="index!=nodeConfig.conditionNodes.length-1" @click.stop="arrTransfer(index)">
<el-icon><el-icon-arrow-right /></el-icon>
</div>
</div>
<add-node v-model="item.childNode"></add-node>
</div>
</div>
<slot v-if="item.childNode" :node="item"></slot>
<div class="top-left-cover-line" v-if="index==0"></div>
<div class="bottom-left-cover-line" v-if="index==0"></div>
<div class="top-right-cover-line" v-if="index==nodeConfig.conditionNodes.length-1"></div>
<div class="bottom-right-cover-line" v-if="index==nodeConfig.conditionNodes.length-1"></div>
</div>
</div>
<add-node v-model="nodeConfig.childNode"></add-node>
</div>
<el-drawer title="条件设置" v-model="drawer" destroy-on-close append-to-body :size="600">
<template #header>
<div class="node-wrap-drawer__title">
<label @click="editTitle" v-if="!isEditTitle">{{form.nodeName}}<el-icon class="node-wrap-drawer__title-edit"><el-icon-edit /></el-icon></label>
<el-input v-if="isEditTitle" ref="nodeTitle" v-model="form.nodeName" clearable @blur="saveTitle" @keyup.enter="saveTitle"></el-input>
</div>
</template>
<el-container>
<el-main style="padding:0 20px 20px 20px">
<el-form label-position="top">
<el-form-item label="条件关系">
<el-radio-group v-model="form.conditionMode">
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
<el-divider></el-divider>
<el-form-item>
<el-table :data="form.conditionList">
<el-table-column prop="label" label="描述">
<template #default="scope">
<el-input v-model="scope.row.label" placeholder="描述"></el-input>
</template>
</el-table-column>
<el-table-column prop="field" label="条件字段" width="130">
<template #default="scope">
<el-input v-model="scope.row.field" placeholder="条件字段"></el-input>
</template>
</el-table-column>
<el-table-column prop="operator" label="运算符" width="130">
<template #default="scope">
<el-select v-model="scope.row.operator" placeholder="Select">
<el-option label="等于" value="="></el-option>
<el-option label="不等于" value="!="></el-option>
<el-option label="大于" value=">"></el-option>
<el-option label="大于等于" value=">="></el-option>
<el-option label="小于" value="<"></el-option>
<el-option label="小于等于" value="<="></el-option>
<el-option label="包含" value="include"></el-option>
<el-option label="不包含" value="notinclude"></el-option>
</el-select>
</template>
</el-table-column>
<el-table-column prop="value" label="值" width="100">
<template #default="scope">
<el-input v-model="scope.row.value" placeholder="值"></el-input>
</template>
</el-table-column>
<el-table-column prop="value" label="移除" width="55">
<template #default="scope">
<el-link type="danger" :underline="false" @click="deleteConditionList(scope.$index)">移除</el-link>
</template>
</el-table-column>
</el-table>
</el-form-item>
<p><el-button type="primary" icon="el-icon-plus" round @click="addConditionList">增加条件</el-button></p>
</el-form>
</el-main>
<el-footer>
<el-button type="primary" @click="save">保存</el-button>
<el-button @click="drawer=false">取消</el-button>
</el-footer>
</el-container>
</el-drawer>
</div>
</template>
<script>
import addNode from './addNode'
export default {
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
addNode
},
data() {
return {
nodeConfig: {},
drawer: false,
isEditTitle: false,
index: 0,
form: {}
}
},
watch:{
modelValue(){
this.nodeConfig = this.modelValue
}
},
mounted() {
this.nodeConfig = this.modelValue
},
methods: {
show(index){
this.index = index
this.form = {}
this.form = JSON.parse(JSON.stringify(this.nodeConfig.conditionNodes[index]))
this.drawer = true
},
editTitle(){
this.isEditTitle = true
this.$nextTick(()=>{
this.$refs.nodeTitle.focus()
})
},
saveTitle(){
this.isEditTitle = false
},
save(){
this.nodeConfig.conditionNodes[this.index] = this.form
this.$emit("update:modelValue", this.nodeConfig)
this.drawer = false
},
addTerm(){
let len = this.nodeConfig.conditionNodes.length + 1
this.nodeConfig.conditionNodes.push({
nodeName: "条件" + len,
type: 3,
priorityLevel: len,
conditionMode: 1,
conditionList: []
})
},
delTerm(index){
this.nodeConfig.conditionNodes.splice(index, 1)
if (this.nodeConfig.conditionNodes.length == 1) {
if (this.nodeConfig.childNode) {
if (this.nodeConfig.conditionNodes[0].childNode) {
this.reData(this.nodeConfig.conditionNodes[0].childNode, this.nodeConfig.childNode)
}else{
this.nodeConfig.conditionNodes[0].childNode = this.nodeConfig.childNode
}
}
this.$emit("update:modelValue", this.nodeConfig.conditionNodes[0].childNode);
}
},
reData(data, addData) {
if (!data.childNode) {
data.childNode = addData
} else {
this.reData(data.childNode, addData)
}
},
arrTransfer(index, type = 1){
this.nodeConfig.conditionNodes[index] = this.nodeConfig.conditionNodes.splice(index + type, 1, this.nodeConfig.conditionNodes[index])[0]
this.nodeConfig.conditionNodes.map((item, index) => {
item.priorityLevel = index + 1
})
this.$emit("update:modelValue", this.nodeConfig)
},
addConditionList(){
this.form.conditionList.push({
label: '',
field: '',
operator: '=',
value: ''
})
},
deleteConditionList(index){
this.form.conditionList.splice(index, 1)
},
toText(nodeConfig, index){
var { conditionList } = nodeConfig.conditionNodes[index]
if (conditionList && conditionList.length == 1) {
const text = conditionList.map(item => `${item.label}${item.operator}${item.value}`).join(" 和 ")
return text
}else if(conditionList && conditionList.length > 1){
const conditionModeText = nodeConfig.conditionNodes[index].conditionMode==1?'且行':'或行'
return conditionList.length + "个条件," + conditionModeText
}else{
if(index == nodeConfig.conditionNodes.length - 1){
return "其他条件进入此流程"
}else{
return false
}
}
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,106 @@
<template>
<div class="node-wrap">
<div class="node-wrap-box start-node" @click="show">
<div class="title" style="background: #576a95;">
<el-icon class="icon"><el-icon-user-filled /></el-icon>
<span>{{ nodeConfig.nodeName }}</span>
</div>
<div class="content">
<span>{{ toText(nodeConfig) }}</span>
</div>
</div>
<add-node v-model="nodeConfig.childNode"></add-node>
<el-drawer title="发起人" v-model="drawer" destroy-on-close append-to-body :size="500">
<template #header>
<div class="node-wrap-drawer__title">
<label @click="editTitle" v-if="!isEditTitle">{{form.nodeName}}<el-icon class="node-wrap-drawer__title-edit"><el-icon-edit /></el-icon></label>
<el-input v-if="isEditTitle" ref="nodeTitle" v-model="form.nodeName" clearable @blur="saveTitle" @keyup.enter="saveTitle"></el-input>
</div>
</template>
<el-container>
<el-main style="padding:0 20px 20px 20px">
<el-form label-position="top">
<el-form-item label="谁可以发起此审批">
<el-button type="primary" icon="el-icon-plus" round @click="selectHandle(2, form.nodeRoleList)">选择角色</el-button>
<div class="tags-list">
<el-tag v-for="(role, index) in form.nodeRoleList" :key="role.id" type="info" closable @close="delRole(index)">{{role.name}}</el-tag>
</div>
</el-form-item>
<el-alert v-if="form.nodeRoleList.length==0" title="不指定则默认所有人都可发起此审批" type="info" :closable="false"/>
</el-form>
</el-main>
<el-footer>
<el-button type="primary" @click="save">保存</el-button>
<el-button @click="drawer=false">取消</el-button>
</el-footer>
</el-container>
</el-drawer>
</div>
</template>
<script>
import addNode from './addNode'
export default {
inject: ['select'],
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
addNode
},
data() {
return {
nodeConfig: {},
drawer: false,
isEditTitle: false,
form: {}
}
},
watch:{
modelValue(){
this.nodeConfig = this.modelValue
}
},
mounted() {
this.nodeConfig = this.modelValue
},
methods: {
show(){
this.form = {}
this.form = JSON.parse(JSON.stringify(this.nodeConfig))
this.isEditTitle = false
this.drawer = true
},
editTitle(){
this.isEditTitle = true
this.$nextTick(()=>{
this.$refs.nodeTitle.focus()
})
},
saveTitle(){
this.isEditTitle = false
},
selectHandle(type, data){
this.select(type, data)
},
delRole(index){
this.form.nodeRoleList.splice(index, 1)
},
save(){
this.$emit("update:modelValue", this.form)
this.drawer = false
},
toText(nodeConfig){
if(nodeConfig.nodeRoleList && nodeConfig.nodeRoleList.length > 0){
return nodeConfig.nodeRoleList.map(item=>item.name).join("、")
}else{
return "所有人"
}
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,118 @@
<template>
<div class="node-wrap">
<div class="node-wrap-box" @click="show">
<div class="title" style="background: #3296fa;">
<el-icon class="icon"><el-icon-promotion /></el-icon>
<span>{{ nodeConfig.nodeName }}</span>
<el-icon class="close" @click.stop="delNode()"><el-icon-close /></el-icon>
</div>
<div class="content">
<span v-if="toText(nodeConfig)">{{ toText(nodeConfig) }}</span>
<span v-else class="placeholder">请选择人员</span>
</div>
</div>
<add-node v-model="nodeConfig.childNode"></add-node>
<el-drawer title="抄送人设置" v-model="drawer" destroy-on-close append-to-body :size="500">
<template #header>
<div class="node-wrap-drawer__title">
<label @click="editTitle" v-if="!isEditTitle">{{form.nodeName}}<el-icon class="node-wrap-drawer__title-edit"><el-icon-edit /></el-icon></label>
<el-input v-if="isEditTitle" ref="nodeTitle" v-model="form.nodeName" clearable @blur="saveTitle" @keyup.enter="saveTitle"></el-input>
</div>
</template>
<el-container>
<el-main style="padding:0 20px 20px 20px">
<el-form label-position="top">
<el-form-item label="选择要抄送的人员">
<el-button type="primary" icon="el-icon-plus" round @click="selectHandle(1, form.nodeUserList)">选择人员</el-button>
<div class="tags-list">
<el-tag v-for="(user, index) in form.nodeUserList" :key="user.id" closable @close="delUser(index)">{{user.name}}</el-tag>
</div>
</el-form-item>
<el-form-item label="">
<el-checkbox v-model="form.userSelectFlag" label="允许发起人自选抄送人"></el-checkbox>
</el-form-item>
</el-form>
</el-main>
<el-footer>
<el-button type="primary" @click="save">保存</el-button>
<el-button @click="drawer=false">取消</el-button>
</el-footer>
</el-container>
</el-drawer>
</div>
</template>
<script>
import addNode from './addNode'
export default {
inject: ['select'],
props: {
modelValue: { type: Object, default: () => {} }
},
components: {
addNode
},
data() {
return {
nodeConfig: {},
drawer: false,
isEditTitle: false,
form: {}
}
},
watch:{
modelValue(){
this.nodeConfig = this.modelValue
}
},
mounted() {
this.nodeConfig = this.modelValue
},
methods: {
show(){
this.form = {}
this.form = JSON.parse(JSON.stringify(this.nodeConfig))
this.drawer = true
},
editTitle(){
this.isEditTitle = true
this.$nextTick(()=>{
this.$refs.nodeTitle.focus()
})
},
saveTitle(){
this.isEditTitle = false
},
save(){
this.$emit("update:modelValue", this.form)
this.drawer = false
},
delNode(){
this.$emit("update:modelValue", this.nodeConfig.childNode)
},
delUser(index){
this.form.nodeUserList.splice(index, 1)
},
selectHandle(type, data){
this.select(type, data)
},
toText(nodeConfig){
if (nodeConfig.nodeUserList && nodeConfig.nodeUserList.length>0) {
const users = nodeConfig.nodeUserList.map(item=>item.name).join("、")
return users
}else{
if(nodeConfig.userSelectFlag){
return "发起人自选"
}else{
return false
}
}
}
}
}
</script>
<style>
</style>
@@ -0,0 +1,269 @@
<template>
<el-dialog v-model="dialogVisible" :title="titleMap[type-1]" :width="type==1?680:460" destroy-on-close append-to-body @closed="$emit('closed')">
<template v-if="type==1">
<div class="sc-user-select">
<div class="sc-user-select__left">
<div class="sc-user-select__search">
<el-input v-model="keyword" prefix-icon="el-icon-search" placeholder="搜索成员">
<template #append>
<el-button icon="el-icon-search" @click="search"></el-button>
</template>
</el-input>
</div>
<div class="sc-user-select__select">
<div class="sc-user-select__tree" v-loading="showGrouploading">
<el-scrollbar>
<el-tree class="menu" ref="groupTree" :data="group" :node-key="groupProps.key" :props="groupProps" highlight-current :expand-on-click-node="false" :current-node-key="groupId" @node-click="groupClick"/>
</el-scrollbar>
</div>
<div class="sc-user-select__user" v-loading="showUserloading">
<div class="sc-user-select__user__list">
<el-scrollbar ref="userScrollbar">
<el-tree class="menu" ref="userTree" :data="user" :node-key="userProps.key" :props="userProps" :default-checked-keys="selectedIds" show-checkbox check-on-click-node @check-change="userClick"></el-tree>
</el-scrollbar>
</div>
<footer>
<el-pagination background layout="prev,next" small :total="total" :page-size="pageSize" v-model:currentPage="currentPage" @current-change="paginationChange"></el-pagination>
</footer>
</div>
</div>
</div>
<div class="sc-user-select__toicon"><el-icon><el-icon-arrow-right /></el-icon></div>
<div class="sc-user-select__selected">
<header>已选 ({{selected.length}})</header>
<ul>
<el-scrollbar>
<li v-for="(item, index) in selected" :key="item.id">
<span class="name">
<el-avatar>{{item.name.substring(0,1)}}</el-avatar>
<label>{{item.name}}</label>
</span>
<span class="delete">
<el-button type="danger" icon="el-icon-delete" circle @click="deleteSelected(index)"></el-button>
</span>
</li>
</el-scrollbar>
</ul>
</div>
</div>
</template>
<template v-if="type==2">
<div class="sc-user-select sc-user-select-role">
<div class="sc-user-select__left">
<div class="sc-user-select__select">
<div class="sc-user-select__tree" v-loading="showGrouploading">
<el-scrollbar>
<el-tree class="menu" ref="groupTree" :data="role" :node-key="roleProps.key" :props="roleProps" show-checkbox check-strictly check-on-click-node :expand-on-click-node="false" :default-checked-keys="selectedIds" @check-change="roleClick"/>
</el-scrollbar>
</div>
</div>
</div>
<div class="sc-user-select__toicon"><el-icon><el-icon-arrow-right /></el-icon></div>
<div class="sc-user-select__selected">
<header>已选 ({{selected.length}})</header>
<ul>
<el-scrollbar>
<li v-for="(item, index) in selected" :key="item.id">
<span class="name">
<label>{{item.name}}</label>
</span>
<span class="delete">
<el-button type="danger" icon="el-icon-delete" circle @click="deleteSelected(index)"></el-button>
</span>
</li>
</el-scrollbar>
</ul>
</div>
</div>
</template>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="save"> </el-button>
</template>
</el-dialog>
</template>
<script>
import config from "@/config/workflow";
export default {
props: {
modelValue: { type: Boolean, default: false }
},
data() {
return {
groupProps: config.group.props,
userProps: config.user.props,
roleProps: config.role.props,
titleMap: ['人员选择', '角色选择'],
dialogVisible: false,
showGrouploading: false,
showUserloading: false,
keyword: '',
groupId: '',
pageSize: config.user.pageSize,
total: 0,
currentPage: 1,
group: [],
user: [],
role: [],
type: 1,
selected: [],
value: []
}
},
computed: {
selectedIds(){
return this.selected.map(t => t.id)
}
},
mounted() {
},
methods: {
//打开赋值
open(type, data){
this.type = type
this.value = data||[]
this.selected = JSON.parse(JSON.stringify(data||[]))
this.dialogVisible = true
if(this.type==1){
this.getGroup()
this.getUser()
}else if(this.type==2){
this.getRole()
}
},
//获取组织
async getGroup(){
this.showGrouploading = true;
var res = await config.group.apiObj.get();
this.showGrouploading = false;
var allNode = {[config.group.props.key]: '', [config.group.props.label]: '所有'}
res.data.unshift(allNode);
this.group = config.group.parseData(res).rows
},
//获取用户
async getUser(){
this.showUserloading = true;
var params = {
[config.user.request.keyword]: this.keyword || null,
[config.user.request.groupId]: this.groupId || null,
[config.user.request.page]: this.currentPage,
[config.user.request.pageSize]: this.pageSize
}
var res = await config.user.apiObj.get(params);
this.showUserloading = false;
this.user = config.user.parseData(res).rows;
this.total = config.user.parseData(res).total || 0;
this.$refs.userScrollbar.setScrollTop(0)
},
//获取角色
async getRole(){
this.showGrouploading = true;
var res = await config.role.apiObj.get();
this.showGrouploading = false;
this.role = config.role.parseData(res).rows
},
//组织点击
groupClick(data){
this.keyword = ''
this.currentPage = 1
this.groupId = data[config.group.props.key]
this.getUser()
},
//用户点击
userClick(data, checked){
if(checked){
this.selected.push({
id: data[config.user.props.key],
name: data[config.user.props.label]
})
}else{
this.selected = this.selected.filter(item => item.id != data[config.user.props.key])
}
},
//用户分页点击
paginationChange(){
this.getUser()
},
//用户搜索
search(){
this.groupId = ''
this.$refs.groupTree.setCurrentKey(this.groupId)
this.currentPage = 1
this.getUser()
},
//删除已选
deleteSelected(index){
this.selected.splice(index,1);
if(this.type==1){
this.$refs.userTree.setCheckedKeys(this.selectedIds)
}else if(this.type==2){
this.$refs.groupTree.setCheckedKeys(this.selectedIds)
}
},
//角色点击
roleClick(data, checked){
if(checked){
this.selected.push({
id: data[config.role.props.key],
name: data[config.role.props.label]
})
}else{
this.selected = this.selected.filter(item => item.id != data[config.role.props.key])
}
},
//提交保存
save(){
this.value.splice(0,this.value.length);
this.selected.map(item => {
this.value.push(item)
})
this.dialogVisible = false
}
}
}
</script>
<style scoped>
.sc-user-select {display: flex;}
.sc-user-select__left {width: 400px;}
.sc-user-select__right {flex: 1;}
.sc-user-select__search {padding-bottom:10px;}
.sc-user-select__select {display: flex;border: 1px solid var(--el-border-color-light);background: var(--el-color-white);}
.sc-user-select__tree {width: 200px;height:300px;border-right: 1px solid var(--el-border-color-light);}
.sc-user-select__user {width: 200px;height:300px;display: flex;flex-direction: column;}
.sc-user-select__user__list {flex: 1;overflow: auto;}
.sc-user-select__user footer {height:36px;padding-top:5px;border-top: 1px solid var(--el-border-color-light);}
.sc-user-select__toicon {display: flex;justify-content: center;align-items: center;margin:0 10px;}
.sc-user-select__toicon i {display: flex;justify-content: center;align-items: center;background: #ccc;width: 20px;height: 20px;text-align: center;line-height: 20px;border-radius:50%;color: #fff;}
.sc-user-select__selected {height:345px;width: 200px;border: 1px solid var(--el-border-color-light);background: var(--el-color-white);}
.sc-user-select__selected header {height:43px;line-height: 43px;border-bottom: 1px solid var(--el-border-color-light);padding:0 15px;font-size: 12px;}
.sc-user-select__selected ul {height:300px;overflow: auto;}
.sc-user-select__selected li {display: flex;align-items: center;justify-content: space-between;padding:5px 5px 5px 15px;height:38px;}
.sc-user-select__selected li .name {display: flex;align-items: center;}
.sc-user-select__selected li .name .el-avatar {background: #409eff;margin-right: 10px;}
.sc-user-select__selected li .name label {}
.sc-user-select__selected li .delete {display: none;}
.sc-user-select__selected li:hover {background: var(--el-color-primary-light-9);}
.sc-user-select__selected li:hover .delete {display: inline-block;}
.sc-user-select-role .sc-user-select__left {width: 200px;}
.sc-user-select-role .sc-user-select__tree {border: none;height: 343px;}
.sc-user-select-role .sc-user-select__selected {}
[data-theme='dark'] .sc-user-select__selected li:hover {background: rgba(0, 0, 0, 0.2);}
[data-theme='dark'] .sc-user-select__toicon i {background: #383838;}
</style>
+69
View File
@@ -0,0 +1,69 @@
import API from "@/api";
//文件选择器配置
export default {
apiObj: API.common.upload,
menuApiObj: API.common.file.menu,
listApiObj: API.common.file.list,
successCode: 1,
maxSize: 30,
max: 99,
uploadParseData: function (res) {
return {
id: res.data.id,
fileName: res.data.fileName,
url: res.data.src
}
},
listParseData: function (res) {
return {
rows: res.data.data,
total: res.data.total,
msg: res.message,
code: res.code
}
},
request: {
page: 'page',
pageSize: 'pageSize',
keyword: 'keyword',
menuKey: 'groupId'
},
menuProps: {
key: 'id',
label: 'title',
children: 'children'
},
fileProps: {
key: 'id',
fileName: 'fileName',
url: 'url'
},
files: {
doc: {
icon: 'sc-icon-file-word-2-fill',
color: '#409eff'
},
docx: {
icon: 'sc-icon-file-word-2-fill',
color: '#409eff'
},
xls: {
icon: 'sc-icon-file-excel-2-fill',
color: '#67C23A'
},
xlsx: {
icon: 'sc-icon-file-excel-2-fill',
color: '#67C23A'
},
ppt: {
icon: 'sc-icon-file-ppt-2-fill',
color: '#F56C6C'
},
pptx: {
icon: 'sc-icon-file-ppt-2-fill',
color: '#F56C6C'
}
}
}
+74
View File
@@ -0,0 +1,74 @@
export default {
//运算符
operator: [
{
label: '等于',
value: '=',
},
{
label: '不等于',
value: '!=',
},
{
label: '大于',
value: '>',
},
{
label: '大于等于',
value: '>=',
},
{
label: '小于',
value: '<',
},
{
label: '小于等于',
value: '<=',
},
{
label: '包含',
value: 'include',
},
{
label: '不包含',
value: 'notinclude',
}
],
//过滤结果运算符的分隔符
separator: '|',
//获取我的常用
getMy: function (name) {
return new Promise((resolve) => {
console.log(`这里可以根据${name}参数请求接口`)
var list = []
setTimeout(()=>{
resolve(list)
},500)
})
},
/**
* 常用保存处理 返回resolve后继续操作
* @name scFilterBar组件的props->filterName
* @obj 过滤项整理好的对象
*/
saveMy: function (name, obj) {
return new Promise((resolve) => {
console.log(name, obj)
setTimeout(()=>{
resolve(true)
},500)
})
},
/**
* 常用删除处理 返回resolve后继续操作
* @name scFilterBar组件的props->filterName
*/
delMy: function (name) {
return new Promise((resolve) => {
console.log(name)
setTimeout(()=>{
resolve(true)
},500)
})
}
}
+308
View File
@@ -0,0 +1,308 @@
//图标选择器配置
export default {
icons: [{
name: '默认',
icons: [
"el-icon-add-location",
"el-icon-aim",
"el-icon-alarm-clock",
"el-icon-apple",
"el-icon-arrow-down",
"el-icon-arrow-down-bold",
"el-icon-arrow-left",
"el-icon-arrow-left-bold",
"el-icon-arrow-right",
"el-icon-arrow-right-bold",
"el-icon-arrow-up",
"el-icon-arrow-up-bold",
"el-icon-avatar",
"el-icon-back",
"el-icon-baseball",
"el-icon-basketball",
"el-icon-bell",
"el-icon-bell-filled",
"el-icon-bicycle",
"el-icon-bottom",
"el-icon-bottom-left",
"el-icon-bottom-right",
"el-icon-bowl",
"el-icon-box",
"el-icon-briefcase",
"el-icon-brush",
"el-icon-brush-filled",
"el-icon-burger",
"el-icon-calendar",
"el-icon-camera",
"el-icon-camera-filled",
"el-icon-caret-bottom",
"el-icon-caret-left",
"el-icon-caret-right",
"el-icon-caret-top",
"el-icon-cellphone",
"el-icon-chat-dot-round",
"el-icon-chat-dot-square",
"el-icon-chat-line-round",
"el-icon-chat-line-square",
"el-icon-chat-round",
"el-icon-chat-square",
"el-icon-check",
"el-icon-checked",
"el-icon-cherry",
"el-icon-chicken",
"el-icon-circle-check",
"el-icon-circle-check-filled",
"el-icon-circle-close",
"el-icon-circle-close-filled",
"el-icon-circle-plus",
"el-icon-circle-plus-filled",
"el-icon-clock",
"el-icon-close",
"el-icon-close-bold",
"el-icon-cloudy",
"el-icon-coffee",
"el-icon-coffee-cup",
"el-icon-coin",
"el-icon-cold-drink",
"el-icon-collection",
"el-icon-collection-tag",
"el-icon-comment",
"el-icon-compass",
"el-icon-connection",
"el-icon-coordinate",
"el-icon-copy-document",
"el-icon-cpu",
"el-icon-credit-card",
"el-icon-crop",
"el-icon-d-arrow-left",
"el-icon-d-arrow-right",
"el-icon-d-caret",
"el-icon-data-analysis",
"el-icon-data-board",
"el-icon-data-line",
"el-icon-delete",
"el-icon-delete-filled",
"el-icon-delete-location",
"el-icon-dessert",
"el-icon-discount",
"el-icon-dish",
"el-icon-dish-dot",
"el-icon-document",
"el-icon-document-add",
"el-icon-document-checked",
"el-icon-document-copy",
"el-icon-document-delete",
"el-icon-document-remove",
"el-icon-download",
"el-icon-drizzling",
"el-icon-edit",
"el-icon-edit-pen",
"el-icon-eleme",
"el-icon-eleme-filled",
"el-icon-element-plus",
"el-icon-expand",
"el-icon-failed",
"el-icon-female",
"el-icon-files",
"el-icon-film",
"el-icon-filter",
"el-icon-finished",
"el-icon-first-aid-kit",
"el-icon-flag",
"el-icon-fold",
"el-icon-folder",
"el-icon-folder-add",
"el-icon-folder-checked",
"el-icon-folder-delete",
"el-icon-folder-opened",
"el-icon-folder-remove",
"el-icon-food",
"el-icon-football",
"el-icon-fork-spoon",
"el-icon-fries",
"el-icon-full-screen",
"el-icon-goblet",
"el-icon-goblet-full",
"el-icon-goblet-square",
"el-icon-goblet-square-full",
"el-icon-goods",
"el-icon-goods-filled",
"el-icon-grape",
"el-icon-grid",
"el-icon-guide",
"el-icon-headset",
"el-icon-help",
"el-icon-help-filled",
"el-icon-hide",
"el-icon-histogram",
"el-icon-home-filled",
"el-icon-hot-water",
"el-icon-house",
"el-icon-ice-cream",
"el-icon-ice-cream-round",
"el-icon-ice-cream-square",
"el-icon-ice-drink",
"el-icon-ice-tea",
"el-icon-info-filled",
"el-icon-iphone",
"el-icon-key",
"el-icon-knife-fork",
"el-icon-lightning",
"el-icon-link",
"el-icon-list",
"el-icon-loading",
"el-icon-location",
"el-icon-location-filled",
"el-icon-location-information",
"el-icon-lock",
"el-icon-lollipop",
"el-icon-magic-stick",
"el-icon-magnet",
"el-icon-male",
"el-icon-management",
"el-icon-map-location",
"el-icon-medal",
"el-icon-menu",
"el-icon-message",
"el-icon-message-box",
"el-icon-mic",
"el-icon-microphone",
"el-icon-milk-tea",
"el-icon-minus",
"el-icon-money",
"el-icon-monitor",
"el-icon-moon",
"el-icon-moon-night",
"el-icon-more",
"el-icon-more-filled",
"el-icon-mostly-cloudy",
"el-icon-mouse",
"el-icon-mug",
"el-icon-mute",
"el-icon-mute-notification",
"el-icon-no-smoking",
"el-icon-notebook",
"el-icon-notification",
"el-icon-odometer",
"el-icon-office-building",
"el-icon-open",
"el-icon-operation",
"el-icon-opportunity",
"el-icon-orange",
"el-icon-paperclip",
"el-icon-partly-cloudy",
"el-icon-pear",
"el-icon-phone",
"el-icon-phone-filled",
"el-icon-picture",
"el-icon-picture-filled",
"el-icon-picture-rounded",
"el-icon-pie-chart",
"el-icon-place",
"el-icon-platform",
"el-icon-plus",
"el-icon-pointer",
"el-icon-position",
"el-icon-postcard",
"el-icon-pouring",
"el-icon-present",
"el-icon-price-tag",
"el-icon-printer",
"el-icon-promotion",
"el-icon-question-filled",
"el-icon-rank",
"el-icon-reading",
"el-icon-reading-lamp",
"el-icon-refresh",
"el-icon-refresh-left",
"el-icon-refresh-right",
"el-icon-refrigerator",
"el-icon-remove",
"el-icon-remove-filled",
"el-icon-right",
"el-icon-scale-to-original",
"el-icon-school",
"el-icon-scissor",
"el-icon-search",
"el-icon-select",
"el-icon-sell",
"el-icon-semi-select",
"el-icon-service",
"el-icon-set-up",
"el-icon-setting",
"el-icon-share",
"el-icon-ship",
"el-icon-shop",
"el-icon-shopping-bag",
"el-icon-shopping-cart",
"el-icon-shopping-cart-full",
"el-icon-smoking",
"el-icon-soccer",
"el-icon-sold-out",
"el-icon-sort",
"el-icon-sort-down",
"el-icon-sort-up",
"el-icon-stamp",
"el-icon-star",
"el-icon-star-filled",
"el-icon-stopwatch",
"el-icon-success-filled",
"el-icon-sugar",
"el-icon-suitcase",
"el-icon-sunny",
"el-icon-sunrise",
"el-icon-sunset",
"el-icon-switch",
"el-icon-switch-button",
"el-icon-takeaway-box",
"el-icon-ticket",
"el-icon-tickets",
"el-icon-timer",
"el-icon-toilet-paper",
"el-icon-tools",
"el-icon-top",
"el-icon-top-left",
"el-icon-top-right",
"el-icon-trend-charts",
"el-icon-trophy",
"el-icon-turn-off",
"el-icon-umbrella",
"el-icon-unlock",
"el-icon-upload",
"el-icon-upload-filled",
"el-icon-user",
"el-icon-user-filled",
"el-icon-van",
"el-icon-video-camera",
"el-icon-video-camera-filled",
"el-icon-video-pause",
"el-icon-video-play",
"el-icon-view",
"el-icon-wallet",
"el-icon-wallet-filled",
"el-icon-warning",
"el-icon-warning-filled",
"el-icon-watch",
"el-icon-watermelon",
"el-icon-wind-power",
"el-icon-zoom-in",
"el-icon-zoom-out"
]
},
{
name: '扩展',
icons: [
'sc-icon-vue',
'sc-icon-code',
'sc-icon-wechat',
'sc-icon-bug-fill',
'sc-icon-bug-line',
'sc-icon-file-word',
'sc-icon-file-excel',
'sc-icon-file-ppt',
'sc-icon-organization',
'sc-icon-upload',
'sc-icon-download'
]
}
]
}
+79
View File
@@ -0,0 +1,79 @@
const DEFAULT_CONFIG = {
//标题
APP_NAME: 'SentOS',
//首页地址
DASHBOARD_URL: "/dashboard",
//版本号
APP_VER: "1.6.6",
//内核版本号
CORE_VER: "1.6.6",
//接口地址
API_URL: 'http://localhost:8000/admin/',
//请求超时
TIMEOUT: 50000,
//TokenName
TOKEN_NAME: "authorization",
//Token前缀,注意最后有个空格,如不需要需设置空字符串
TOKEN_PREFIX: "Bearer ",
//追加其他头
HEADERS: {},
//请求是否开启缓存
REQUEST_CACHE: false,
//布局 默认:default | 通栏:header | 经典:menu | 功能坞:dock
//dock将关闭标签和面包屑栏
LAYOUT: 'default',
//菜单是否折叠
MENU_IS_COLLAPSE: false,
//菜单是否启用手风琴效果
MENU_UNIQUE_OPENED: false,
//是否开启多标签
LAYOUT_TAGS: true,
//语言
LANG: 'zh-cn',
//主题颜色
COLOR: '',
//是否加密localStorage, 为空不加密,可填写AES(模式ECB,移位Pkcs7)加密
LS_ENCRYPTION: '',
//localStorageAES加密秘钥,位数建议填写8的倍数
LS_ENCRYPTION_key: '2XNN4K8LC0ELVWN4',
//控制台首页默认布局
DEFAULT_GRID: {
//默认分栏数量和宽度 例如 [24] [18,6] [8,8,8] [6,12,6]
layout: [24, 12, 12],
//小组件分布,com取值:pages/home/components 文件名
copmsList: [
['welcome'],
['info'],
['ver']
]
}
}
//合并业务配置
import MY_CONFIG from "./myConfig"
Object.assign(DEFAULT_CONFIG, MY_CONFIG)
// 如果生产模式,就合并动态的APP_CONFIG
// public/config.js
if(process.env.NODE_ENV === 'production'){
Object.assign(DEFAULT_CONFIG, APP_CONFIG)
}
export default DEFAULT_CONFIG
+21
View File
@@ -0,0 +1,21 @@
//业务配置
//会合并至this.$CONFIG
//生产模式 public/config.js 同名key会覆盖这里的配置从而实现打包后的热更新
//为避免和SCUI框架配置混淆建议添加前缀 MY_
//全局可使用 this.$CONFIG.MY_KEY 访问
export default {
//是否显示第三方授权登录
MY_SHOW_LOGIN_OAUTH: false,
shopTemplateList: [
{
value: 'default',
label: '默认模版',
},
{
value: 'action',
label: '活动模版'
}
]
}
+31
View File
@@ -0,0 +1,31 @@
// 静态路由配置
// 书写格式与动态路由格式一致,全部经由框架统一转换
// 比较动态路由在meta中多加入了role角色权限,为数组类型。一个菜单是否有权限显示,取决于它以及后代菜单是否有权限。
// routes 显示在左侧菜单中的路由(显示顺序在动态路由之前)
// 示例如下
// const routes = [
// {
// name: "demo",
// path: "/demo",
// meta: {
// icon: "el-icon-eleme-filled",
// title: "演示",
// role: ["SA"]
// },
// children: [{
// name: "demopage",
// path: "/demopage",
// component: "test/autocode/index",
// meta: {
// icon: "el-icon-menu",
// title: "演示页面",
// role: ["SA"]
// }
// }]
// }
// ]
const routes = []
export default routes;
+21
View File
@@ -0,0 +1,21 @@
import API from "@/api";
//字典选择器配置
export default {
dicApiObj: API.system.dictionary.list, //获取字典接口对象
parseData: function (res) {
return {
data: res.data, //分析行数据字段结构
msg: res.message, //分析描述字段结构
code: res.code //分析状态字段结构
}
},
request: {
name: 'name' //规定搜索字段
},
props: {
label: 'title', //映射label显示字段
value: 'values', //映射value值字段
}
}
+70
View File
@@ -0,0 +1,70 @@
//数据表格配置
import tool from '@/utils/tool'
export default {
successCode: 1, //请求完成代码
pageSize: 30, //表格每一页条数
pageSizes: [30, 100, 200, 500], //表格可设置的一页条数
paginationLayout: "total, sizes, prev, pager, next, jumper", //表格分页布局,可设置"total, sizes, prev, pager, next, jumper"
parseData: function (res) { //数据分析
return {
data: res.data, //分析无分页的数据字段结构
rows: res.data.data, //分析行数据字段结构
total: res.data.total, //分析总数字段结构
summary: res.data.summary, //分析合计行字段结构
msg: res.message, //分析描述字段结构
code: res.code //分析状态字段结构
}
},
request: { //请求规定字段
page: 'page', //规定当前分页字段
pageSize: 'limit', //规定一页条数字段
prop: 'prop', //规定排序字段名字段
order: 'order' //规定排序规格字段
},
/**
* 自定义列保存处理
* @tableName scTable组件的props->tableName
* @column 用户配置好的列
*/
columnSettingSave: function (tableName, column) {
return new Promise((resolve) => {
setTimeout(()=>{
//这里为了演示使用了session和setTimeout演示,开发时应用数据请求
tool.session.set(tableName, column)
resolve(true)
},1000)
})
},
/**
* 获取自定义列
* @tableName scTable组件的props->tableName
* @column 组件接受到的props->column
*/
columnSettingGet: function (tableName, column) {
return new Promise((resolve) => {
//这里为了演示使用了session和setTimeout演示,开发时应用数据请求
const userColumn = tool.session.get(tableName)
if(userColumn){
resolve(userColumn)
}else{
resolve(column)
}
})
},
/**
* 重置自定义列
* @tableName scTable组件的props->tableName
* @column 组件接受到的props->column
*/
columnSettingReset: function (tableName, column) {
return new Promise((resolve) => {
//这里为了演示使用了session和setTimeout演示,开发时应用数据请求
setTimeout(()=>{
tool.session.remove(tableName)
resolve(column)
},1000)
})
}
}
+23
View File
@@ -0,0 +1,23 @@
//表格选择器配置
export default {
pageSize: 100, //表格每一页条数
parseData: function (res) {
return {
data: res.data,
rows: res.data.data, //分析行数据字段结构
total: res.data.total, //分析总数字段结构
msg: res.message, //分析描述字段结构
code: res.code //分析状态字段结构
}
},
request: {
page: 'page', //规定当前分页字段
pageSize: 'pageSize', //规定一页条数字段
keyword: 'keyword' //规定搜索字段
},
props: {
label: 'name', //映射label显示字段
value: 'id', //映射value值字段
}
}
+20
View File
@@ -0,0 +1,20 @@
import API from "@/api";
//上传配置
export default {
apiObj: API.common.upload, //上传请求API对象
filename: "file", //form请求时文件的key
successCode: 1, //请求完成代码
maxSize: 10, //最大文件大小 默认10MB
parseData: function (res) {
return {
code: res.code, //分析状态字段结构
fileName: res.data.name,//分析文件名称
src: res.data.url, //分析图片远程地址结构
msg: res.message //分析描述字段结构
}
},
apiObjFile: API.common.upload, //附件上传请求API对象
maxSizeFile: 10 //最大文件大小 默认10MB
}
+69
View File
@@ -0,0 +1,69 @@
import API from "@/api";
//审批工作流人员/组织选择器配置
export default {
//配置接口正常返回代码
successCode: 200,
//配置组织
group: {
//请求接口对象
apiObj: API.system.dept.list,
//接受数据字段映射
parseData: function (res) {
return {
rows: res.data,
msg: res.message,
code: res.code
}
},
//显示数据字段映射
props: {
key: 'id',
label: 'label',
children: 'children'
}
},
//配置用户
user: {
apiObj: API.demo.page,
pageSize: 20,
parseData: function (res) {
return {
rows: res.data.rows,
total: res.data.total,
msg: res.message,
code: res.code
}
},
props: {
key: 'id',
label: 'user',
},
request: {
page: 'page',
pageSize: 'pageSize',
groupId: 'groupId',
keyword: 'keyword'
}
},
//配置角色
role: {
//请求接口对象
apiObj: API.system.dept.list,
//接受数据字段映射
parseData: function (res) {
return {
rows: res.data,
msg: res.message,
code: res.code
}
},
//显示数据字段映射
props: {
key: 'id',
label: 'label',
children: 'children'
}
}
}
+22
View File
@@ -0,0 +1,22 @@
import { permission } from '@/utils/permission'
export default {
mounted(el, binding) {
const { value } = binding
if(Array.isArray(value)){
let ishas = false;
value.forEach(item => {
if(permission(item)){
ishas = true;
}
})
if (!ishas){
el.parentNode.removeChild(el)
}
}else{
if(!permission(value)){
el.parentNode.removeChild(el);
}
}
}
};
+29
View File
@@ -0,0 +1,29 @@
import { ElMessage } from 'element-plus'
export default {
mounted(el, binding) {
el.$value = binding.value
el.handler = () => {
const textarea = document.createElement('textarea')
textarea.readOnly = 'readonly'
textarea.style.position = 'absolute'
textarea.style.left = '-9999px'
textarea.value = el.$value
document.body.appendChild(textarea)
textarea.select()
textarea.setSelectionRange(0, textarea.value.length)
const result = document.execCommand('Copy')
if (result) {
ElMessage.success("复制成功")
}
document.body.removeChild(textarea)
}
el.addEventListener('click', el.handler)
},
updated(el, binding){
el.$value = binding.value
},
unmounted(el){
el.removeEventListener('click', el.handler)
}
}

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