前端代码从vue2升级到vue3

This commit is contained in:
dataprince 2023-09-05 16:18:41 +08:00
parent 79b3e3a751
commit ec2781d166
151 changed files with 7448 additions and 13097 deletions

22
.gitignore vendored
View File

@ -48,3 +48,25 @@ nbdist/
!*/build/*.xml !*/build/*.xml
.flattened-pom.xml .flattened-pom.xml
DS_Store
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
**/*.log
tests/**/coverage/
tests/e2e/reports
selenium-debug.log
# Editor directories and files
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.local
package-lock.json
yarn.lock

View File

@ -1,7 +1,7 @@
# 项目相关配置 # 项目相关配置
ruoyi: ruoyi:
# 名称 # 名称
name: RuoYi-Flex name: Ruoyi-Flex
# 版本 # 版本
version: 4.1.6 version: 4.1.6
# 版权年份 # 版权年份
@ -217,6 +217,7 @@ security:
- /**/*.html - /**/*.html
- /**/*.css - /**/*.css
- /**/*.js - /**/*.js
- /profile/**
# 公共路径 # 公共路径
- /favicon.ico - /favicon.ico
- /error - /error

View File

@ -10,7 +10,6 @@ import org.springframework.stereotype.Component;
* *
* @author ruoyi * @author ruoyi
*/ */
@Data
@Component @Component
@ConfigurationProperties(prefix = "ruoyi") @ConfigurationProperties(prefix = "ruoyi")
public class RuoYiConfig { public class RuoYiConfig {
@ -43,11 +42,46 @@ public class RuoYiConfig {
/** /**
* 获取地址开关 * 获取地址开关
*/ */
@Getter
private static boolean addressEnabled; private static boolean addressEnabled;
public void setAddressEnabled(boolean addressEnabled) { public String getName()
RuoYiConfig.addressEnabled = addressEnabled; {
return name;
}
public void setName(String name)
{
this.name = name;
}
public String getVersion()
{
return version;
}
public void setVersion(String version)
{
this.version = version;
}
public String getCopyrightYear()
{
return copyrightYear;
}
public void setCopyrightYear(String copyrightYear)
{
this.copyrightYear = copyrightYear;
}
public boolean isDemoEnabled()
{
return demoEnabled;
}
public void setDemoEnabled(boolean demoEnabled)
{
this.demoEnabled = demoEnabled;
} }
public static String getProfile() public static String getProfile()
@ -55,6 +89,22 @@ public class RuoYiConfig {
return profile; return profile;
} }
public void setProfile(String profile)
{
RuoYiConfig.profile = profile;
}
public static boolean isAddressEnabled()
{
return addressEnabled;
}
public void setAddressEnabled(boolean addressEnabled)
{
RuoYiConfig.addressEnabled = addressEnabled;
}
/** /**
* 获取导入上传路径 * 获取导入上传路径
*/ */

View File

@ -17,6 +17,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
/** /**
* 权限安全配置 * 权限安全配置
* *
@ -38,12 +40,32 @@ public class SecurityConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
// 注册路由拦截器自定义验证规则 // 注册路由拦截器自定义验证规则
registry.addInterceptor(new SaInterceptor(handler -> { registry.addInterceptor(new SaInterceptor(handler -> {
AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class); //AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class);
// 登录验证 -- 排除多个路径 // 登录验证 -- 排除多个路径
SaRouter SaRouter
// 获取所有的 // 获取所有的
.match(allUrlHandler.getUrls()) //.match(allUrlHandler.getUrls()) // 拦截的 path 列表
// 对未排除的路径进行检查 .match("/**")
.notMatch(
Arrays.asList(
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/**/*.css",
"/**/*.js",
"/profile/**",
"/favicon.ico",
"/error",
"/*/api-docs",
"/*/api-docs/**",
"/actuator",
"/actuator/**",
"/login",
"/register",
"/captchaImage",
"/captcha/get",
"/captcha/check"))
.check(() -> { .check(() -> {
// 检查是否登录 是否有token // 检查是否登录 是否有token
StpUtil.checkLogin(); StpUtil.checkLogin();
@ -51,9 +73,9 @@ public class SecurityConfig implements WebMvcConfigurer {
//TODO :以后完善多平台登录校验clientID功能 //TODO :以后完善多平台登录校验clientID功能
}); });
})).addPathPatterns("/**") })).addPathPatterns("/**");
// 排除不需要拦截的路径 // 排除不需要拦截的路径
.excludePathPatterns(securityProperties.getExcludes()); //.excludePathPatterns(securityProperties.getExcludes());
} }
} }

View File

@ -1,4 +1,4 @@
com.ruoyi.common.security.config.SaTokenConfig com.ruoyi.common.security.config.SaTokenConfig
com.ruoyi.common.security.config.SecurityConfig
com.ruoyi.common.security.handler.GlobalExceptionHandler com.ruoyi.common.security.handler.GlobalExceptionHandler
com.ruoyi.common.security.handler.AllUrlHandler com.ruoyi.common.security.handler.AllUrlHandler
com.ruoyi.common.security.config.SecurityConfig

View File

@ -26,7 +26,7 @@ oms.http.port=10010
oms.table-prefix=pj_ oms.table-prefix=pj_
# Actuator 监控端点的配置项 # Actuator 监控端点的配置项
spring.application.name: ruoyi-powerjob-server spring.application.name: Ruoyi-Powerjob-Server
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=ALWAYS management.endpoint.health.show-details=ALWAYS
management.endpoint.logfile.external-file=./logs/ruoyi-powerjob-server.log management.endpoint.logfile.external-file=./logs/ruoyi-powerjob-server.log

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
#foreach($column in $columns) #foreach($column in $columns)
#if($column.query) #if($column.query)
#set($dictType=$column.dictType) #set($dictType=$column.dictType)
@ -17,14 +17,14 @@
v-model="queryParams.${column.javaField}" v-model="queryParams.${column.javaField}"
placeholder="请输入${comment}" placeholder="请输入${comment}"
clearable clearable
@keyup.enter.native="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType) #elseif(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType)
<el-form-item label="${comment}" prop="${column.javaField}"> <el-form-item label="${comment}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable> <el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
<el-option <el-option
v-for="dict in dict.type.${dictType}" v-for="dict in ${dictType}"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -42,16 +42,15 @@
<el-date-picker clearable <el-date-picker clearable
v-model="queryParams.${column.javaField}" v-model="queryParams.${column.javaField}"
type="date" type="date"
value-format="yyyy-MM-dd" value-format="YYYY-MM-DD"
placeholder="选择${comment}"> placeholder="选择${comment}">
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
#elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN") #elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
<el-form-item label="${comment}"> <el-form-item label="${comment}" style="width: 308px">
<el-date-picker <el-date-picker
v-model="daterange${AttrName}" v-model="daterange${AttrName}"
style="width: 240px" value-format="YYYY-MM-DD"
value-format="yyyy-MM-dd"
type="daterange" type="daterange"
range-separator="-" range-separator="-"
start-placeholder="开始日期" start-placeholder="开始日期"
@ -62,8 +61,8 @@
#end #end
#end #end
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -72,8 +71,7 @@
<el-button <el-button
type="primary" type="primary"
plain plain
icon="el-icon-plus" icon="Plus"
size="mini"
@click="handleAdd" @click="handleAdd"
v-hasPermi="['${moduleName}:${businessName}:add']" v-hasPermi="['${moduleName}:${businessName}:add']"
>新增</el-button> >新增</el-button>
@ -82,12 +80,11 @@
<el-button <el-button
type="info" type="info"
plain plain
icon="el-icon-sort" icon="Sort"
size="mini"
@click="toggleExpandAll" @click="toggleExpandAll"
>展开/折叠</el-button> >展开/折叠</el-button>
</el-col> </el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
<el-table <el-table
@ -109,23 +106,23 @@
#if($column.pk) #if($column.pk)
#elseif($column.list && $column.htmlType == "datetime") #elseif($column.list && $column.htmlType == "datetime")
<el-table-column label="${comment}" align="center" prop="${javaField}" width="180"> <el-table-column label="${comment}" align="center" prop="${javaField}" width="180">
<template slot-scope="scope"> <template #default="scope">
<span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
#elseif($column.list && $column.htmlType == "imageUpload") #elseif($column.list && $column.htmlType == "imageUpload")
<el-table-column label="${comment}" align="center" prop="${javaField}" width="100"> <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
<template slot-scope="scope"> <template #default="scope">
<image-preview :src="scope.row.${javaField}" :width="50" :height="50"/> <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
</template> </template>
</el-table-column> </el-table-column>
#elseif($column.list && "" != $column.dictType) #elseif($column.list && "" != $column.dictType)
<el-table-column label="${comment}" align="center" prop="${javaField}"> <el-table-column label="${comment}" align="center" prop="${javaField}">
<template slot-scope="scope"> <template #default="scope">
#if($column.htmlType == "checkbox") #if($column.htmlType == "checkbox")
<dict-tag :options="dict.type.${column.dictType}" :value="scope.row.${javaField} ? scope.row.${javaField}.split(',') : []"/> <dict-tag :options="${column.dictType}" :value="scope.row.${javaField} ? scope.row.${javaField}.split(',') : []"/>
#else #else
<dict-tag :options="dict.type.${column.dictType}" :value="scope.row.${javaField}"/> <dict-tag :options="${column.dictType}" :value="scope.row.${javaField}"/>
#end #end
</template> </template>
</el-table-column> </el-table-column>
@ -138,35 +135,17 @@
#end #end
#end #end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope"> <template #default="scope">
<el-button <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']">修改</el-button>
size="mini" <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['${moduleName}:${businessName}:add']">新增</el-button>
type="text" <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${moduleName}:${businessName}:remove']">删除</el-button>
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['${moduleName}:${businessName}:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-plus"
@click="handleAdd(scope.row)"
v-hasPermi="['${moduleName}:${businessName}:add']"
>新增</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['${moduleName}:${businessName}:remove']"
>删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 添加或修改${functionName}对话框 --> <!-- 添加或修改${functionName}对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body> <el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> <el-form ref="${businessName}Ref" :model="form" :rules="rules" label-width="80px">
#foreach($column in $columns) #foreach($column in $columns)
#set($field=$column.javaField) #set($field=$column.javaField)
#if($column.insert && !$column.pk) #if($column.insert && !$column.pk)
@ -180,7 +159,14 @@
#set($dictType=$column.dictType) #set($dictType=$column.dictType)
#if("" != $treeParentCode && $column.javaField == $treeParentCode) #if("" != $treeParentCode && $column.javaField == $treeParentCode)
<el-form-item label="${comment}" prop="${treeParentCode}"> <el-form-item label="${comment}" prop="${treeParentCode}">
<treeselect v-model="form.${treeParentCode}" :options="${businessName}Options" :normalizer="normalizer" placeholder="请选择${comment}" /> <el-tree-select
v-model="form.${treeParentCode}"
:data="${businessName}Options"
:props="{ value: '${treeCode}', label: '${treeName}', children: 'children' }"
value-key="${treeCode}"
placeholder="请选择${comment}"
check-strictly
/>
</el-form-item> </el-form-item>
#elseif($column.htmlType == "input") #elseif($column.htmlType == "input")
<el-form-item label="${comment}" prop="${field}"> <el-form-item label="${comment}" prop="${field}">
@ -202,7 +188,7 @@
<el-form-item label="${comment}" prop="${field}"> <el-form-item label="${comment}" prop="${field}">
<el-select v-model="form.${field}" placeholder="请选择${comment}"> <el-select v-model="form.${field}" placeholder="请选择${comment}">
<el-option <el-option
v-for="dict in dict.type.${dictType}" v-for="dict in ${dictType}"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
#if($column.javaType == "Integer" || $column.javaType == "Long") #if($column.javaType == "Integer" || $column.javaType == "Long")
@ -223,7 +209,7 @@
<el-form-item label="${comment}" prop="${field}"> <el-form-item label="${comment}" prop="${field}">
<el-checkbox-group v-model="form.${field}"> <el-checkbox-group v-model="form.${field}">
<el-checkbox <el-checkbox
v-for="dict in dict.type.${dictType}" v-for="dict in ${dictType}"
:key="dict.value" :key="dict.value"
:label="dict.value"> :label="dict.value">
{{dict.label}} {{dict.label}}
@ -240,7 +226,7 @@
<el-form-item label="${comment}" prop="${field}"> <el-form-item label="${comment}" prop="${field}">
<el-radio-group v-model="form.${field}"> <el-radio-group v-model="form.${field}">
<el-radio <el-radio
v-for="dict in dict.type.${dictType}" v-for="dict in ${dictType}"
:key="dict.value" :key="dict.value"
#if($column.javaType == "Integer" || $column.javaType == "Long") #if($column.javaType == "Integer" || $column.javaType == "Long")
:label="parseInt(dict.value)" :label="parseInt(dict.value)"
@ -261,7 +247,7 @@
<el-date-picker clearable <el-date-picker clearable
v-model="form.${field}" v-model="form.${field}"
type="date" type="date"
value-format="yyyy-MM-dd" value-format="YYYY-MM-DD"
placeholder="选择${comment}"> placeholder="选择${comment}">
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
@ -274,65 +260,59 @@
#end #end
#end #end
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <template #footer>
<el-button type="primary" @click="submitForm">确 定</el-button> <div class="dialog-footer">
<el-button @click="cancel">取 消</el-button> <el-button type="primary" @click="submitForm">确 定</el-button>
</div> <el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script> <script setup name="${BusinessName}">
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}"; import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
export default { const { proxy } = getCurrentInstance();
name: "${BusinessName}",
#if(${dicts} != '') #if(${dicts} != '')
dicts: [${dicts}], #set($dictsNoSymbol=$dicts.replace("'", ""))
const { ${dictsNoSymbol} } = proxy.useDict(${dicts});
#end #end
components: {
Treeselect // 树表表格数据
}, const ${businessName}List = ref([]);
data() { // 树选项
return { const ${businessName}Options = ref([]);
// 遮罩层 // 是否显示弹出层
loading: true, const open = ref(false);
// 显示搜索条件 // 遮罩层
showSearch: true, const loading = ref(true);
// ${functionName}表格数据 // 显示搜索条件
${businessName}List: [], const showSearch = ref(true);
// ${functionName}树选项 // 弹出层标题
${businessName}Options: [], const title = ref("");
// 弹出层标题 // 是否展开,默认全部展开
title: "", const isExpandAll = ref(true);
// 是否显示弹出层 // 重新渲染表格状态
open: false, const refreshTable = ref(true);
// 是否展开,默认全部展开
isExpandAll: true,
// 重新渲染表格状态
refreshTable: true,
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN") #if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
// $comment时间范围 const daterange${AttrName} = ref([]);
daterange${AttrName}: [],
#end #end
#end #end
// 查询参数
queryParams: { const data = reactive({
#foreach ($column in $columns) form: {},
queryParams: {
#foreach ($column in $columns)
#if($column.query) #if($column.query)
$column.javaField: null#if($foreach.count != $columns.size()),#end $column.javaField: null#if($foreach.count != $columns.size()),#end
#end #end
#end #end
}, },
// 表单参数 rules: {
form: {}, #foreach ($column in $columns)
// 表单校验
rules: {
#foreach ($column in $columns)
#if($column.required) #if($column.required)
#set($parentheseIndex=$column.columnComment.indexOf("")) #set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1) #if($parentheseIndex != -1)
@ -340,166 +320,157 @@ export default {
#else #else
#set($comment=$column.columnComment) #set($comment=$column.columnComment)
#end #end
$column.javaField: [ $column.javaField: [
{ required: true, message: "$comment不能为空", trigger: #if($column.htmlType == "select" || $column.htmlType == "radio")"change"#else"blur"#end } { required: true, message: "$comment不能为空", trigger: #if($column.htmlType == "select" || $column.htmlType == "radio")"change"#else"blur"#end }
]#if($foreach.count != $columns.size()),#end ]#if($foreach.count != $columns.size()),#end
#end #end
#end #end
} }
}; });
},
created() { const { queryParams, form, rules } = toRefs(data);
this.getList();
}, /** 查询${functionName}列表 */
methods: { function getList() {
/** 查询${functionName}列表 */ loading.value = true;
getList() {
this.loading = true;
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN") #if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
this.queryParams.params = {}; queryParams.value.params = {};
#break #break
#end #end
#end #end
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN") #if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
if (null != this.daterange${AttrName} && '' != this.daterange${AttrName}) { if (null != daterange${AttrName} && '' != daterange${AttrName}) {
this.queryParams.params["begin${AttrName}"] = this.daterange${AttrName}[0]; queryParams.value.params["begin${AttrName}"] = daterange${AttrName}.value[0];
this.queryParams.params["end${AttrName}"] = this.daterange${AttrName}[1]; queryParams.value.params["end${AttrName}"] = daterange${AttrName}.value[1];
} }
#end #end
#end #end
list${BusinessName}(this.queryParams).then(response => { list${BusinessName}(queryParams.value).then(response => {
this.${businessName}List = this.handleTree(response.data, "${treeCode}", "${treeParentCode}"); ${businessName}List.value = proxy.handleTree(response.data, "${treeCode}", "${treeParentCode}");
this.loading = false; loading.value = false;
}); });
}, }
/** 转换${functionName}数据结构 */
normalizer(node) { // 取消按钮
if (node.children && !node.children.length) { function cancel() {
delete node.children; open.value = false;
} reset();
return { }
id: node.${treeCode},
label: node.${treeName}, // 表单重置
children: node.children function reset() {
}; form.value = {
},
/** 查询${functionName}下拉树结构 */
getTreeselect() {
list${BusinessName}().then(response => {
this.${businessName}Options = [];
const data = { ${treeCode}: 0, ${treeName}: '顶级节点', children: [] };
data.children = this.handleTree(response.data, "${treeCode}", "${treeParentCode}");
this.${businessName}Options.push(data);
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "checkbox") #if($column.htmlType == "checkbox")
$column.javaField: []#if($foreach.count != $columns.size()),#end $column.javaField: []#if($foreach.count != $columns.size()),#end
#else #else
$column.javaField: null#if($foreach.count != $columns.size()),#end $column.javaField: null#if($foreach.count != $columns.size()),#end
#end #end
#end #end
}; };
this.resetForm("form"); proxy.resetForm("${businessName}Ref");
}, }
/** 搜索按钮操作 */
handleQuery() { /** 搜索按钮操作 */
this.getList(); function handleQuery() {
}, getList();
/** 重置按钮操作 */ }
resetQuery() {
/** 重置按钮操作 */
function resetQuery() {
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN") #if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
this.daterange${AttrName} = []; daterange${AttrName}.value = [];
#end #end
#end #end
this.resetForm("queryForm"); proxy.resetForm("queryRef");
this.handleQuery(); handleQuery();
}, }
/** 新增按钮操作 */
handleAdd(row) { /** 新增按钮操作 */
this.reset(); async function handleAdd(row) {
this.getTreeselect(); reset();
if (row != null && row.${treeCode}) { await list${BusinessName}().then(response => {
this.form.${treeParentCode} = row.${treeCode}; ${businessName}Options.value = proxy.handleTree(response.data, "${treeCode}");
} else { });
this.form.${treeParentCode} = 0; if (row != null && row.${treeCode}) {
} form.value.${treeParentCode} = row.${treeCode};
this.open = true; } else {
this.title = "添加${functionName}"; form.value.${treeParentCode} = 0;
},
/** 展开/折叠操作 */
toggleExpandAll() {
this.refreshTable = false;
this.isExpandAll = !this.isExpandAll;
this.$nextTick(() => {
this.refreshTable = true;
});
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
this.getTreeselect();
if (row != null) {
this.form.${treeParentCode} = row.${treeCode};
}
get${BusinessName}(row.${pkColumn.javaField}).then(response => {
this.form = response.data;
#foreach ($column in $columns)
#if($column.htmlType == "checkbox")
this.form.$column.javaField = this.form.${column.javaField}.split(",");
#end
#end
this.open = true;
this.title = "修改${functionName}";
});
},
/** 提交按钮 */
submitForm() {
this.#[[$]]#refs["form"].validate(valid => {
if (valid) {
#foreach ($column in $columns)
#if($column.htmlType == "checkbox")
this.form.$column.javaField = this.form.${column.javaField}.join(",");
#end
#end
if (this.form.${pkColumn.javaField} != null) {
update${BusinessName}(this.form).then(response => {
this.#[[$modal]]#.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} else {
add${BusinessName}(this.form).then(response => {
this.#[[$modal]]#.msgSuccess("新增成功");
this.open = false;
this.getList();
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
this.#[[$modal]]#.confirm('是否确认删除${functionName}编号为"' + row.${pkColumn.javaField} + '"的数据项?').then(function() {
return del${BusinessName}(row.${pkColumn.javaField});
}).then(() => {
this.getList();
this.#[[$modal]]#.msgSuccess("删除成功");
}).catch(() => {});
}
} }
}; open.value = true;
title.value = "添加${functionName}";
}
/** 展开/折叠操作 */
function toggleExpandAll() {
refreshTable.value = false;
isExpandAll.value = !isExpandAll.value;
nextTick(() => {
refreshTable.value = true;
});
}
/** 修改按钮操作 */
async function handleUpdate(row) {
reset();
await listProduct().then(response => {
${businessName}Options.value = proxy.handleTree(response.data, "${treeCode}");
});
if (row != null) {
form.value.${treeParentCode} = row.${treeCode};
}
get${BusinessName}(row.${pkColumn.javaField}).then(response => {
form.value = response.data;
#foreach ($column in $columns)
#if($column.htmlType == "checkbox")
form.value.$column.javaField = form.value.${column.javaField}.split(",");
#end
#end
open.value = true;
title.value = "修改${functionName}";
});
}
/** 提交按钮 */
function submitForm() {
proxy.#[[$]]#refs["${businessName}Ref"].validate(valid => {
if (valid) {
#foreach ($column in $columns)
#if($column.htmlType == "checkbox")
form.value.$column.javaField = form.value.${column.javaField}.join(",");
#end
#end
if (form.value.${pkColumn.javaField} != null) {
update${BusinessName}(form.value).then(response => {
proxy.#[[$modal]]#.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
add${BusinessName}(form.value).then(response => {
proxy.#[[$modal]]#.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
proxy.#[[$modal]]#.confirm('是否确认删除${functionName}编号为"' + row.${pkColumn.javaField} + '"的数据项?').then(function() {
return del${BusinessName}(row.${pkColumn.javaField});
}).then(() => {
getList();
proxy.#[[$modal]]#.msgSuccess("删除成功");
}).catch(() => {});
}
getList();
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
#foreach($column in $columns) #foreach($column in $columns)
#if($column.query) #if($column.query)
#set($dictType=$column.dictType) #set($dictType=$column.dictType)
@ -17,14 +17,14 @@
v-model="queryParams.${column.javaField}" v-model="queryParams.${column.javaField}"
placeholder="请输入${comment}" placeholder="请输入${comment}"
clearable clearable
@keyup.enter.native="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType) #elseif(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType)
<el-form-item label="${comment}" prop="${column.javaField}"> <el-form-item label="${comment}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable> <el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
<el-option <el-option
v-for="dict in dict.type.${dictType}" v-for="dict in ${dictType}"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -42,16 +42,15 @@
<el-date-picker clearable <el-date-picker clearable
v-model="queryParams.${column.javaField}" v-model="queryParams.${column.javaField}"
type="date" type="date"
value-format="yyyy-MM-dd" value-format="YYYY-MM-DD"
placeholder="请选择${comment}"> placeholder="请选择${comment}">
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
#elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN") #elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
<el-form-item label="${comment}"> <el-form-item label="${comment}" style="width: 308px">
<el-date-picker <el-date-picker
v-model="daterange${AttrName}" v-model="daterange${AttrName}"
style="width: 240px" value-format="YYYY-MM-DD"
value-format="yyyy-MM-dd"
type="daterange" type="daterange"
range-separator="-" range-separator="-"
start-placeholder="开始日期" start-placeholder="开始日期"
@ -62,8 +61,8 @@
#end #end
#end #end
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -72,8 +71,7 @@
<el-button <el-button
type="primary" type="primary"
plain plain
icon="el-icon-plus" icon="Plus"
size="mini"
@click="handleAdd" @click="handleAdd"
v-hasPermi="['${moduleName}:${businessName}:add']" v-hasPermi="['${moduleName}:${businessName}:add']"
>新增</el-button> >新增</el-button>
@ -82,8 +80,7 @@
<el-button <el-button
type="success" type="success"
plain plain
icon="el-icon-edit" icon="Edit"
size="mini"
:disabled="single" :disabled="single"
@click="handleUpdate" @click="handleUpdate"
v-hasPermi="['${moduleName}:${businessName}:edit']" v-hasPermi="['${moduleName}:${businessName}:edit']"
@ -93,8 +90,7 @@
<el-button <el-button
type="danger" type="danger"
plain plain
icon="el-icon-delete" icon="Delete"
size="mini"
:disabled="multiple" :disabled="multiple"
@click="handleDelete" @click="handleDelete"
v-hasPermi="['${moduleName}:${businessName}:remove']" v-hasPermi="['${moduleName}:${businessName}:remove']"
@ -104,13 +100,12 @@
<el-button <el-button
type="warning" type="warning"
plain plain
icon="el-icon-download" icon="Download"
size="mini"
@click="handleExport" @click="handleExport"
v-hasPermi="['${moduleName}:${businessName}:export']" v-hasPermi="['${moduleName}:${businessName}:export']"
>导出</el-button> >导出</el-button>
</el-col> </el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
<el-table v-loading="loading" :data="${businessName}List" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="${businessName}List" @selection-change="handleSelectionChange">
@ -127,23 +122,23 @@
<el-table-column label="${comment}" align="center" prop="${javaField}" /> <el-table-column label="${comment}" align="center" prop="${javaField}" />
#elseif($column.list && $column.htmlType == "datetime") #elseif($column.list && $column.htmlType == "datetime")
<el-table-column label="${comment}" align="center" prop="${javaField}" width="180"> <el-table-column label="${comment}" align="center" prop="${javaField}" width="180">
<template slot-scope="scope"> <template #default="scope">
<span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
#elseif($column.list && $column.htmlType == "imageUpload") #elseif($column.list && $column.htmlType == "imageUpload")
<el-table-column label="${comment}" align="center" prop="${javaField}" width="100"> <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
<template slot-scope="scope"> <template #default="scope">
<image-preview :src="scope.row.${javaField}" :width="50" :height="50"/> <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
</template> </template>
</el-table-column> </el-table-column>
#elseif($column.list && "" != $column.dictType) #elseif($column.list && "" != $column.dictType)
<el-table-column label="${comment}" align="center" prop="${javaField}"> <el-table-column label="${comment}" align="center" prop="${javaField}">
<template slot-scope="scope"> <template #default="scope">
#if($column.htmlType == "checkbox") #if($column.htmlType == "checkbox")
<dict-tag :options="dict.type.${column.dictType}" :value="scope.row.${javaField} ? scope.row.${javaField}.split(',') : []"/> <dict-tag :options="${column.dictType}" :value="scope.row.${javaField} ? scope.row.${javaField}.split(',') : []"/>
#else #else
<dict-tag :options="dict.type.${column.dictType}" :value="scope.row.${javaField}"/> <dict-tag :options="${column.dictType}" :value="scope.row.${javaField}"/>
#end #end
</template> </template>
</el-table-column> </el-table-column>
@ -152,36 +147,24 @@
#end #end
#end #end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope"> <template #default="scope">
<el-button <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']">修改</el-button>
size="mini" <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${moduleName}:${businessName}:remove']">删除</el-button>
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['${moduleName}:${businessName}:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['${moduleName}:${businessName}:remove']"
>删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<pagination <pagination
v-show="total>0" v-show="total>0"
:total="total" :total="total"
:page.sync="queryParams.pageNum" v-model:page="queryParams.pageNum"
:limit.sync="queryParams.pageSize" v-model:limit="queryParams.pageSize"
@pagination="getList" @pagination="getList"
/> />
<!-- 添加或修改${functionName}对话框 --> <!-- 添加或修改${functionName}对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body> <el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> <el-form ref="${businessName}Ref" :model="form" :rules="rules" label-width="80px">
#foreach($column in $columns) #foreach($column in $columns)
#set($field=$column.javaField) #set($field=$column.javaField)
#if($column.insert && !$column.pk) #if($column.insert && !$column.pk)
@ -213,7 +196,7 @@
<el-form-item label="${comment}" prop="${field}"> <el-form-item label="${comment}" prop="${field}">
<el-select v-model="form.${field}" placeholder="请选择${comment}"> <el-select v-model="form.${field}" placeholder="请选择${comment}">
<el-option <el-option
v-for="dict in dict.type.${dictType}" v-for="dict in ${dictType}"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
#if($column.javaType == "Integer" || $column.javaType == "Long") #if($column.javaType == "Integer" || $column.javaType == "Long")
@ -234,7 +217,7 @@
<el-form-item label="${comment}" prop="${field}"> <el-form-item label="${comment}" prop="${field}">
<el-checkbox-group v-model="form.${field}"> <el-checkbox-group v-model="form.${field}">
<el-checkbox <el-checkbox
v-for="dict in dict.type.${dictType}" v-for="dict in ${dictType}"
:key="dict.value" :key="dict.value"
:label="dict.value"> :label="dict.value">
{{dict.label}} {{dict.label}}
@ -251,7 +234,7 @@
<el-form-item label="${comment}" prop="${field}"> <el-form-item label="${comment}" prop="${field}">
<el-radio-group v-model="form.${field}"> <el-radio-group v-model="form.${field}">
<el-radio <el-radio
v-for="dict in dict.type.${dictType}" v-for="dict in ${dictType}"
:key="dict.value" :key="dict.value"
#if($column.javaType == "Integer" || $column.javaType == "Long") #if($column.javaType == "Integer" || $column.javaType == "Long")
:label="parseInt(dict.value)" :label="parseInt(dict.value)"
@ -272,7 +255,7 @@
<el-date-picker clearable <el-date-picker clearable
v-model="form.${field}" v-model="form.${field}"
type="date" type="date"
value-format="yyyy-MM-dd" value-format="YYYY-MM-DD"
placeholder="请选择${comment}"> placeholder="请选择${comment}">
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
@ -288,10 +271,10 @@
<el-divider content-position="center">${subTable.functionName}信息</el-divider> <el-divider content-position="center">${subTable.functionName}信息</el-divider>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd${subClassName}">添加</el-button> <el-button type="primary" icon="Plus" @click="handleAdd${subClassName}">添加</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="danger" icon="el-icon-delete" size="mini" @click="handleDelete${subClassName}">删除</el-button> <el-button type="danger" icon="Delete" @click="handleDelete${subClassName}">删除</el-button>
</el-col> </el-col>
</el-row> </el-row>
<el-table :data="${subclassName}List" :row-class-name="row${subClassName}Index" @selection-change="handle${subClassName}SelectionChange" ref="${subclassName}"> <el-table :data="${subclassName}List" :row-class-name="row${subClassName}Index" @selection-change="handle${subClassName}SelectionChange" ref="${subclassName}">
@ -308,22 +291,27 @@
#if($column.pk || $javaField == ${subTableFkclassName}) #if($column.pk || $javaField == ${subTableFkclassName})
#elseif($column.list && $column.htmlType == "input") #elseif($column.list && $column.htmlType == "input")
<el-table-column label="$comment" prop="${javaField}" width="150"> <el-table-column label="$comment" prop="${javaField}" width="150">
<template slot-scope="scope"> <template #default="scope">
<el-input v-model="scope.row.$javaField" placeholder="请输入$comment" /> <el-input v-model="scope.row.$javaField" placeholder="请输入$comment" />
</template> </template>
</el-table-column> </el-table-column>
#elseif($column.list && $column.htmlType == "datetime") #elseif($column.list && $column.htmlType == "datetime")
<el-table-column label="$comment" prop="${javaField}" width="240"> <el-table-column label="$comment" prop="${javaField}" width="240">
<template slot-scope="scope"> <template #default="scope">
<el-date-picker clearable v-model="scope.row.$javaField" type="date" value-format="yyyy-MM-dd" placeholder="请选择$comment" /> <el-date-picker clearable
v-model="scope.row.$javaField"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择$comment">
</el-date-picker>
</template> </template>
</el-table-column> </el-table-column>
#elseif($column.list && ($column.htmlType == "select" || $column.htmlType == "radio") && "" != $column.dictType) #elseif($column.list && ($column.htmlType == "select" || $column.htmlType == "radio") && "" != $column.dictType)
<el-table-column label="$comment" prop="${javaField}" width="150"> <el-table-column label="$comment" prop="${javaField}" width="150">
<template slot-scope="scope"> <template #default="scope">
<el-select v-model="scope.row.$javaField" placeholder="请选择$comment"> <el-select v-model="scope.row.$javaField" placeholder="请选择$comment">
<el-option <el-option
v-for="dict in dict.type.$column.dictType" v-for="dict in $column.dictType"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -333,7 +321,7 @@
</el-table-column> </el-table-column>
#elseif($column.list && ($column.htmlType == "select" || $column.htmlType == "radio") && "" == $column.dictType) #elseif($column.list && ($column.htmlType == "select" || $column.htmlType == "radio") && "" == $column.dictType)
<el-table-column label="$comment" prop="${javaField}" width="150"> <el-table-column label="$comment" prop="${javaField}" width="150">
<template slot-scope="scope"> <template #default="scope">
<el-select v-model="scope.row.$javaField" placeholder="请选择$comment"> <el-select v-model="scope.row.$javaField" placeholder="请选择$comment">
<el-option label="请选择字典生成" value="" /> <el-option label="请选择字典生成" value="" />
</el-select> </el-select>
@ -344,72 +332,71 @@
</el-table> </el-table>
#end #end
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <template #footer>
<el-button type="primary" @click="submitForm">确 定</el-button> <div class="dialog-footer">
<el-button @click="cancel">取 消</el-button> <el-button type="primary" @click="submitForm">确 定</el-button>
</div> <el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script> <script setup name="${BusinessName}">
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}"; import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}";
export default { const { proxy } = getCurrentInstance();
name: "${BusinessName}",
#if(${dicts} != '') #if(${dicts} != '')
dicts: [${dicts}], #set($dictsNoSymbol=$dicts.replace("'", ""))
const { ${dictsNoSymbol} } = proxy.useDict(${dicts});
#end #end
data() {
return { // 表格数据
// 遮罩层 const ${businessName}List = ref([]);
loading: true,
// 选中数组
ids: [],
#if($table.sub) #if($table.sub)
// 子表选中数据 // 子表格数据
checked${subClassName}: [], const ${subclassName}List = ref([]);
#end #end
// 非单个禁用 // 是否显示弹出层
single: true, const open = ref(false);
// 非多个禁用 // 遮罩层
multiple: true, const loading = ref(true);
// 显示搜索条件 // 显示搜索条件
showSearch: true, const showSearch = ref(true);
// 总条数 // 选中数组
total: 0, const ids = ref([]);
// ${functionName}表格数据
${businessName}List: [],
#if($table.sub) #if($table.sub)
// ${subTable.functionName}表格数据 // 子表选中数据
${subclassName}List: [], const checked${subClassName} = ref([]);
#end #end
// 弹出层标题 // 非单个禁用
title: "", const single = ref(true);
// 是否显示弹出层 // 非多个禁用
open: false, const multiple = ref(true);
// 总条数
const total = ref(0);
// 弹出层标题
const title = ref("");
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN") #if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
// $comment时间范围 const daterange${AttrName} = ref([]);
daterange${AttrName}: [],
#end #end
#end #end
// 查询参数
queryParams: { const data = reactive({
pageNum: 1, form: {},
pageSize: 10, queryParams: {
#foreach ($column in $columns) pageNum: 1,
pageSize: 10,
#foreach ($column in $columns)
#if($column.query) #if($column.query)
$column.javaField: null#if($foreach.count != $columns.size()),#end $column.javaField: null#if($foreach.count != $columns.size()),#end
#end #end
#end #end
}, },
// 表单参数 rules: {
form: {}, #foreach ($column in $columns)
// 表单校验
rules: {
#foreach ($column in $columns)
#if($column.required) #if($column.required)
#set($parentheseIndex=$column.columnComment.indexOf("")) #set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1) #if($parentheseIndex != -1)
@ -417,186 +404,198 @@ export default {
#else #else
#set($comment=$column.columnComment) #set($comment=$column.columnComment)
#end #end
$column.javaField: [ $column.javaField: [
{ required: true, message: "$comment不能为空", trigger: #if($column.htmlType == "select" || $column.htmlType == "radio")"change"#else"blur"#end } { required: true, message: "$comment不能为空", trigger: #if($column.htmlType == "select" || $column.htmlType == "radio")"change"#else"blur"#end }
]#if($foreach.count != $columns.size()),#end ]#if($foreach.count != $columns.size()),#end
#end #end
#end #end
} }
}; });
},
created() { const { queryParams, form, rules } = toRefs(data);
this.getList();
}, /** 查询${functionName}列表 */
methods: { function getList() {
/** 查询${functionName}列表 */ loading.value = true;
getList() {
this.loading = true;
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN") #if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
this.queryParams.params = {}; queryParams.value.params = {};
#break #break
#end #end
#end #end
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN") #if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
if (null != this.daterange${AttrName} && '' != this.daterange${AttrName}) { if (null != daterange${AttrName} && '' != daterange${AttrName}) {
this.queryParams.params["begin${AttrName}"] = this.daterange${AttrName}[0]; queryParams.value.params["begin${AttrName}"] = daterange${AttrName}.value[0];
this.queryParams.params["end${AttrName}"] = this.daterange${AttrName}[1]; queryParams.value.params["end${AttrName}"] = daterange${AttrName}.value[1];
} }
#end #end
#end #end
list${BusinessName}(this.queryParams).then(response => { list${BusinessName}(queryParams.value).then(response => {
this.${businessName}List = response.rows; ${businessName}List.value = response.rows;
this.total = response.total; total.value = response.total;
this.loading = false; loading.value = false;
}); });
}, }
// 取消按钮
cancel() { // 取消按钮
this.open = false; function cancel() {
this.reset(); open.value = false;
}, reset();
// 表单重置 }
reset() {
this.form = { // 表单重置
function reset() {
form.value = {
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "checkbox") #if($column.htmlType == "checkbox")
$column.javaField: []#if($foreach.count != $columns.size()),#end $column.javaField: []#if($foreach.count != $columns.size()),#end
#else #else
$column.javaField: null#if($foreach.count != $columns.size()),#end $column.javaField: null#if($foreach.count != $columns.size()),#end
#end #end
#end #end
}; };
#if($table.sub) #if($table.sub)
this.${subclassName}List = []; ${subclassName}List.value = [];
#end #end
this.resetForm("form"); proxy.resetForm("${businessName}Ref");
}, }
/** 搜索按钮操作 */
handleQuery() { /** 搜索按钮操作 */
this.queryParams.pageNum = 1; function handleQuery() {
this.getList(); queryParams.value.pageNum = 1;
}, getList();
/** 重置按钮操作 */ }
resetQuery() {
/** 重置按钮操作 */
function resetQuery() {
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN") #if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) #set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
this.daterange${AttrName} = []; daterange${AttrName}.value = [];
#end #end
#end #end
this.resetForm("queryForm"); proxy.resetForm("queryRef");
this.handleQuery(); handleQuery();
}, }
// 多选框选中数据
handleSelectionChange(selection) { // 多选框选中数据
this.ids = selection.map(item => item.${pkColumn.javaField}) function handleSelectionChange(selection) {
this.single = selection.length!==1 ids.value = selection.map(item => item.${pkColumn.javaField});
this.multiple = !selection.length single.value = selection.length != 1;
}, multiple.value = !selection.length;
/** 新增按钮操作 */ }
handleAdd() {
this.reset(); /** 新增按钮操作 */
this.open = true; function handleAdd() {
this.title = "添加${functionName}"; reset();
}, open.value = true;
/** 修改按钮操作 */ title.value = "添加${functionName}";
handleUpdate(row) { }
this.reset();
const ${pkColumn.javaField} = row.${pkColumn.javaField} || this.ids /** 修改按钮操作 */
get${BusinessName}(${pkColumn.javaField}).then(response => { function handleUpdate(row) {
this.form = response.data; reset();
const _${pkColumn.javaField} = row.${pkColumn.javaField} || ids.value
get${BusinessName}(_${pkColumn.javaField}).then(response => {
form.value = response.data;
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "checkbox") #if($column.htmlType == "checkbox")
this.form.$column.javaField = this.form.${column.javaField}.split(","); form.value.$column.javaField = form.value.${column.javaField}.split(",");
#end #end
#end #end
#if($table.sub) #if($table.sub)
this.${subclassName}List = response.data.${subclassName}List; ${subclassName}List.value = response.data.${subclassName}List;
#end #end
this.open = true; open.value = true;
this.title = "修改${functionName}"; title.value = "修改${functionName}";
}); });
}, }
/** 提交按钮 */
submitForm() { /** 提交按钮 */
this.#[[$]]#refs["form"].validate(valid => { function submitForm() {
if (valid) { proxy.#[[$]]#refs["${businessName}Ref"].validate(valid => {
if (valid) {
#foreach ($column in $columns) #foreach ($column in $columns)
#if($column.htmlType == "checkbox") #if($column.htmlType == "checkbox")
this.form.$column.javaField = this.form.${column.javaField}.join(","); form.value.$column.javaField = form.value.${column.javaField}.join(",");
#end #end
#end #end
#if($table.sub) #if($table.sub)
this.form.${subclassName}List = this.${subclassName}List; form.value.${subclassName}List = ${subclassName}List.value;
#end #end
if (this.form.${pkColumn.javaField} != null) { if (form.value.${pkColumn.javaField} != null) {
update${BusinessName}(this.form).then(response => { update${BusinessName}(form.value).then(response => {
this.#[[$modal]]#.msgSuccess("修改成功"); proxy.#[[$modal]]#.msgSuccess("修改成功");
this.open = false; open.value = false;
this.getList(); getList();
}); });
} else { } else {
add${BusinessName}(this.form).then(response => { add${BusinessName}(form.value).then(response => {
this.#[[$modal]]#.msgSuccess("新增成功"); proxy.#[[$modal]]#.msgSuccess("新增成功");
this.open = false; open.value = false;
this.getList(); getList();
}); });
} }
} }
}); });
}, }
/** 删除按钮操作 */
handleDelete(row) { /** 删除按钮操作 */
const ${pkColumn.javaField}s = row.${pkColumn.javaField} || this.ids; function handleDelete(row) {
this.#[[$modal]]#.confirm('是否确认删除${functionName}编号为"' + ${pkColumn.javaField}s + '"的数据项?').then(function() { const _${pkColumn.javaField}s = row.${pkColumn.javaField} || ids.value;
return del${BusinessName}(${pkColumn.javaField}s); proxy.#[[$modal]]#.confirm('是否确认删除${functionName}编号为"' + _${pkColumn.javaField}s + '"的数据项?').then(function() {
}).then(() => { return del${BusinessName}(_${pkColumn.javaField}s);
this.getList(); }).then(() => {
this.#[[$modal]]#.msgSuccess("删除成功"); getList();
}).catch(() => {}); proxy.#[[$modal]]#.msgSuccess("删除成功");
}, }).catch(() => {});
}
#if($table.sub) #if($table.sub)
/** ${subTable.functionName}序号 */ /** ${subTable.functionName}序号 */
row${subClassName}Index({ row, rowIndex }) { function row${subClassName}Index({ row, rowIndex }) {
row.index = rowIndex + 1; row.index = rowIndex + 1;
}, }
/** ${subTable.functionName}添加按钮操作 */
handleAdd${subClassName}() { /** ${subTable.functionName}添加按钮操作 */
let obj = {}; function handleAdd${subClassName}() {
let obj = {};
#foreach($column in $subTable.columns) #foreach($column in $subTable.columns)
#if($column.pk || $column.javaField == ${subTableFkclassName}) #if($column.pk || $column.javaField == ${subTableFkclassName})
#elseif($column.list && "" != $javaField) #elseif($column.list && "" != $javaField)
obj.$column.javaField = ""; obj.$column.javaField = "";
#end #end
#end #end
this.${subclassName}List.push(obj); ${subclassName}List.value.push(obj);
}, }
/** ${subTable.functionName}删除按钮操作 */
handleDelete${subClassName}() { /** ${subTable.functionName}删除按钮操作 */
if (this.checked${subClassName}.length == 0) { function handleDelete${subClassName}() {
this.#[[$modal]]#.msgError("请先选择要删除的${subTable.functionName}数据"); if (checked${subClassName}.value.length == 0) {
} else { proxy.#[[$modal]]#.msgError("请先选择要删除的${subTable.functionName}数据");
const ${subclassName}List = this.${subclassName}List; } else {
const checked${subClassName} = this.checked${subClassName}; const ${subclassName}s = ${subclassName}List.value;
this.${subclassName}List = ${subclassName}List.filter(function(item) { const checked${subClassName}s = checked${subClassName}.value;
return checked${subClassName}.indexOf(item.index) == -1 ${subclassName}List.value = ${subclassName}s.filter(function(item) {
}); return checked${subClassName}s.indexOf(item.index) == -1
} });
},
/** 复选框选中数据 */
handle${subClassName}SelectionChange(selection) {
this.checked${subClassName} = selection.map(item => item.index)
},
#end
/** 导出按钮操作 */
handleExport() {
this.download('${moduleName}/${businessName}/export', {
...this.queryParams
}, `${businessName}_#[[${new Date().getTime()}]]#.xlsx`)
}
} }
}; }
/** 复选框选中数据 */
function handle${subClassName}SelectionChange(selection) {
checked${subClassName}.value = selection.map(item => item.index)
}
#end
/** 导出按钮操作 */
function handleExport() {
proxy.download('${moduleName}/${businessName}/export', {
...queryParams.value
}, `${businessName}_#[[${new Date().getTime()}]]#.xlsx`)
}
getList();
</script> </script>

View File

@ -1,474 +0,0 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
#foreach($column in $columns)
#if($column.query)
#set($dictType=$column.dictType)
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#if($column.htmlType == "input")
<el-form-item label="${comment}" prop="${column.javaField}">
<el-input
v-model="queryParams.${column.javaField}"
placeholder="请输入${comment}"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType)
<el-form-item label="${comment}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
<el-option
v-for="dict in ${dictType}"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && $dictType)
<el-form-item label="${comment}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
#elseif($column.htmlType == "datetime" && $column.queryType != "BETWEEN")
<el-form-item label="${comment}" prop="${column.javaField}">
<el-date-picker clearable
v-model="queryParams.${column.javaField}"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择${comment}">
</el-date-picker>
</el-form-item>
#elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
<el-form-item label="${comment}" style="width: 308px">
<el-date-picker
v-model="daterange${AttrName}"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
#end
#end
#end
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['${moduleName}:${businessName}:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="Sort"
@click="toggleExpandAll"
>展开/折叠</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table
v-if="refreshTable"
v-loading="loading"
:data="${businessName}List"
row-key="${treeCode}"
:default-expand-all="isExpandAll"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
>
#foreach($column in $columns)
#set($javaField=$column.javaField)
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#if($column.pk)
#elseif($column.list && $column.htmlType == "datetime")
<el-table-column label="${comment}" align="center" prop="${javaField}" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
#elseif($column.list && $column.htmlType == "imageUpload")
<el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
<template #default="scope">
<image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
</template>
</el-table-column>
#elseif($column.list && "" != $column.dictType)
<el-table-column label="${comment}" align="center" prop="${javaField}">
<template #default="scope">
#if($column.htmlType == "checkbox")
<dict-tag :options="${column.dictType}" :value="scope.row.${javaField} ? scope.row.${javaField}.split(',') : []"/>
#else
<dict-tag :options="${column.dictType}" :value="scope.row.${javaField}"/>
#end
</template>
</el-table-column>
#elseif($column.list && "" != $javaField)
#if(${foreach.index} == 1)
<el-table-column label="${comment}" prop="${javaField}" />
#else
<el-table-column label="${comment}" align="center" prop="${javaField}" />
#end
#end
#end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']">修改</el-button>
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['${moduleName}:${businessName}:add']">新增</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${moduleName}:${businessName}:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加或修改${functionName}对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="${businessName}Ref" :model="form" :rules="rules" label-width="80px">
#foreach($column in $columns)
#set($field=$column.javaField)
#if($column.insert && !$column.pk)
#if(($column.usableColumn) || (!$column.superColumn))
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#set($dictType=$column.dictType)
#if("" != $treeParentCode && $column.javaField == $treeParentCode)
<el-form-item label="${comment}" prop="${treeParentCode}">
<el-tree-select
v-model="form.${treeParentCode}"
:data="${businessName}Options"
:props="{ value: '${treeCode}', label: '${treeName}', children: 'children' }"
value-key="${treeCode}"
placeholder="请选择${comment}"
check-strictly
/>
</el-form-item>
#elseif($column.htmlType == "input")
<el-form-item label="${comment}" prop="${field}">
<el-input v-model="form.${field}" placeholder="请输入${comment}" />
</el-form-item>
#elseif($column.htmlType == "imageUpload")
<el-form-item label="${comment}" prop="${field}">
<image-upload v-model="form.${field}"/>
</el-form-item>
#elseif($column.htmlType == "fileUpload")
<el-form-item label="${comment}" prop="${field}">
<file-upload v-model="form.${field}"/>
</el-form-item>
#elseif($column.htmlType == "editor")
<el-form-item label="${comment}">
<editor v-model="form.${field}" :min-height="192"/>
</el-form-item>
#elseif($column.htmlType == "select" && "" != $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-select v-model="form.${field}" placeholder="请选择${comment}">
<el-option
v-for="dict in ${dictType}"
:key="dict.value"
:label="dict.label"
#if($column.javaType == "Integer" || $column.javaType == "Long")
:value="parseInt(dict.value)"
#else
:value="dict.value"
#end
></el-option>
</el-select>
</el-form-item>
#elseif($column.htmlType == "select" && $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-select v-model="form.${field}" placeholder="请选择${comment}">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
#elseif($column.htmlType == "checkbox" && "" != $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-checkbox-group v-model="form.${field}">
<el-checkbox
v-for="dict in ${dictType}"
:key="dict.value"
:label="dict.value">
{{dict.label}}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
#elseif($column.htmlType == "checkbox" && $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-checkbox-group v-model="form.${field}">
<el-checkbox>请选择字典生成</el-checkbox>
</el-checkbox-group>
</el-form-item>
#elseif($column.htmlType == "radio" && "" != $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-radio-group v-model="form.${field}">
<el-radio
v-for="dict in ${dictType}"
:key="dict.value"
#if($column.javaType == "Integer" || $column.javaType == "Long")
:label="parseInt(dict.value)"
#else
:label="dict.value"
#end
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
#elseif($column.htmlType == "radio" && $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-radio-group v-model="form.${field}">
<el-radio label="1">请选择字典生成</el-radio>
</el-radio-group>
</el-form-item>
#elseif($column.htmlType == "datetime")
<el-form-item label="${comment}" prop="${field}">
<el-date-picker clearable
v-model="form.${field}"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择${comment}">
</el-date-picker>
</el-form-item>
#elseif($column.htmlType == "textarea")
<el-form-item label="${comment}" prop="${field}">
<el-input v-model="form.${field}" type="textarea" placeholder="请输入内容" />
</el-form-item>
#end
#end
#end
#end
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="${BusinessName}">
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}";
const { proxy } = getCurrentInstance();
#if(${dicts} != '')
#set($dictsNoSymbol=$dicts.replace("'", ""))
const { ${dictsNoSymbol} } = proxy.useDict(${dicts});
#end
const ${businessName}List = ref([]);
const ${businessName}Options = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const title = ref("");
const isExpandAll = ref(true);
const refreshTable = ref(true);
#foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
const daterange${AttrName} = ref([]);
#end
#end
const data = reactive({
form: {},
queryParams: {
#foreach ($column in $columns)
#if($column.query)
$column.javaField: null#if($foreach.count != $columns.size()),#end
#end
#end
},
rules: {
#foreach ($column in $columns)
#if($column.required)
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
$column.javaField: [
{ required: true, message: "$comment不能为空", trigger: #if($column.htmlType == "select" || $column.htmlType == "radio")"change"#else"blur"#end }
]#if($foreach.count != $columns.size()),#end
#end
#end
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询${functionName}列表 */
function getList() {
loading.value = true;
#foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
queryParams.value.params = {};
#break
#end
#end
#foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
if (null != daterange${AttrName} && '' != daterange${AttrName}) {
queryParams.value.params["begin${AttrName}"] = daterange${AttrName}.value[0];
queryParams.value.params["end${AttrName}"] = daterange${AttrName}.value[1];
}
#end
#end
list${BusinessName}(queryParams.value).then(response => {
${businessName}List.value = proxy.handleTree(response.data, "${treeCode}", "${treeParentCode}");
loading.value = false;
});
}
/** 查询${functionName}下拉树结构 */
function getTreeselect() {
list${BusinessName}().then(response => {
${businessName}Options.value = [];
const data = { ${treeCode}: 0, ${treeName}: '顶级节点', children: [] };
data.children = proxy.handleTree(response.data, "${treeCode}", "${treeParentCode}");
${businessName}Options.value.push(data);
});
}
// 取消按钮
function cancel() {
open.value = false;
reset();
}
// 表单重置
function reset() {
form.value = {
#foreach ($column in $columns)
#if($column.htmlType == "checkbox")
$column.javaField: []#if($foreach.count != $columns.size()),#end
#else
$column.javaField: null#if($foreach.count != $columns.size()),#end
#end
#end
};
proxy.resetForm("${businessName}Ref");
}
/** 搜索按钮操作 */
function handleQuery() {
getList();
}
/** 重置按钮操作 */
function resetQuery() {
#foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
daterange${AttrName}.value = [];
#end
#end
proxy.resetForm("queryRef");
handleQuery();
}
/** 新增按钮操作 */
function handleAdd(row) {
reset();
getTreeselect();
if (row != null && row.${treeCode}) {
form.value.${treeParentCode} = row.${treeCode};
} else {
form.value.${treeParentCode} = 0;
}
open.value = true;
title.value = "添加${functionName}";
}
/** 展开/折叠操作 */
function toggleExpandAll() {
refreshTable.value = false;
isExpandAll.value = !isExpandAll.value;
nextTick(() => {
refreshTable.value = true;
});
}
/** 修改按钮操作 */
async function handleUpdate(row) {
reset();
await getTreeselect();
if (row != null) {
form.value.${treeParentCode} = row.${treeCode};
}
get${BusinessName}(row.${pkColumn.javaField}).then(response => {
form.value = response.data;
#foreach ($column in $columns)
#if($column.htmlType == "checkbox")
form.value.$column.javaField = form.value.${column.javaField}.split(",");
#end
#end
open.value = true;
title.value = "修改${functionName}";
});
}
/** 提交按钮 */
function submitForm() {
proxy.#[[$]]#refs["${businessName}Ref"].validate(valid => {
if (valid) {
#foreach ($column in $columns)
#if($column.htmlType == "checkbox")
form.value.$column.javaField = form.value.${column.javaField}.join(",");
#end
#end
if (form.value.${pkColumn.javaField} != null) {
update${BusinessName}(form.value).then(response => {
proxy.#[[$modal]]#.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
add${BusinessName}(form.value).then(response => {
proxy.#[[$modal]]#.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
proxy.#[[$modal]]#.confirm('是否确认删除${functionName}编号为"' + row.${pkColumn.javaField} + '"的数据项?').then(function() {
return del${BusinessName}(row.${pkColumn.javaField});
}).then(() => {
getList();
proxy.#[[$modal]]#.msgSuccess("删除成功");
}).catch(() => {});
}
getList();
</script>

View File

@ -1,590 +0,0 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
#foreach($column in $columns)
#if($column.query)
#set($dictType=$column.dictType)
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#if($column.htmlType == "input")
<el-form-item label="${comment}" prop="${column.javaField}">
<el-input
v-model="queryParams.${column.javaField}"
placeholder="请输入${comment}"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && "" != $dictType)
<el-form-item label="${comment}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
<el-option
v-for="dict in ${dictType}"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
#elseif(($column.htmlType == "select" || $column.htmlType == "radio") && $dictType)
<el-form-item label="${comment}" prop="${column.javaField}">
<el-select v-model="queryParams.${column.javaField}" placeholder="请选择${comment}" clearable>
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
#elseif($column.htmlType == "datetime" && $column.queryType != "BETWEEN")
<el-form-item label="${comment}" prop="${column.javaField}">
<el-date-picker clearable
v-model="queryParams.${column.javaField}"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择${comment}">
</el-date-picker>
</el-form-item>
#elseif($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
<el-form-item label="${comment}" style="width: 308px">
<el-date-picker
v-model="daterange${AttrName}"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
</el-form-item>
#end
#end
#end
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['${moduleName}:${businessName}:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
v-hasPermi="['${moduleName}:${businessName}:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['${moduleName}:${businessName}:remove']"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
v-hasPermi="['${moduleName}:${businessName}:export']"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="${businessName}List" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
#foreach($column in $columns)
#set($javaField=$column.javaField)
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#if($column.pk)
<el-table-column label="${comment}" align="center" prop="${javaField}" />
#elseif($column.list && $column.htmlType == "datetime")
<el-table-column label="${comment}" align="center" prop="${javaField}" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
#elseif($column.list && $column.htmlType == "imageUpload")
<el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
<template #default="scope">
<image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
</template>
</el-table-column>
#elseif($column.list && "" != $column.dictType)
<el-table-column label="${comment}" align="center" prop="${javaField}">
<template #default="scope">
#if($column.htmlType == "checkbox")
<dict-tag :options="${column.dictType}" :value="scope.row.${javaField} ? scope.row.${javaField}.split(',') : []"/>
#else
<dict-tag :options="${column.dictType}" :value="scope.row.${javaField}"/>
#end
</template>
</el-table-column>
#elseif($column.list && "" != $javaField)
<el-table-column label="${comment}" align="center" prop="${javaField}" />
#end
#end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${moduleName}:${businessName}:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改${functionName}对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="${businessName}Ref" :model="form" :rules="rules" label-width="80px">
#foreach($column in $columns)
#set($field=$column.javaField)
#if($column.insert && !$column.pk)
#if(($column.usableColumn) || (!$column.superColumn))
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#set($dictType=$column.dictType)
#if($column.htmlType == "input")
<el-form-item label="${comment}" prop="${field}">
<el-input v-model="form.${field}" placeholder="请输入${comment}" />
</el-form-item>
#elseif($column.htmlType == "imageUpload")
<el-form-item label="${comment}" prop="${field}">
<image-upload v-model="form.${field}"/>
</el-form-item>
#elseif($column.htmlType == "fileUpload")
<el-form-item label="${comment}" prop="${field}">
<file-upload v-model="form.${field}"/>
</el-form-item>
#elseif($column.htmlType == "editor")
<el-form-item label="${comment}">
<editor v-model="form.${field}" :min-height="192"/>
</el-form-item>
#elseif($column.htmlType == "select" && "" != $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-select v-model="form.${field}" placeholder="请选择${comment}">
<el-option
v-for="dict in ${dictType}"
:key="dict.value"
:label="dict.label"
#if($column.javaType == "Integer" || $column.javaType == "Long")
:value="parseInt(dict.value)"
#else
:value="dict.value"
#end
></el-option>
</el-select>
</el-form-item>
#elseif($column.htmlType == "select" && $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-select v-model="form.${field}" placeholder="请选择${comment}">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
#elseif($column.htmlType == "checkbox" && "" != $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-checkbox-group v-model="form.${field}">
<el-checkbox
v-for="dict in ${dictType}"
:key="dict.value"
:label="dict.value">
{{dict.label}}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
#elseif($column.htmlType == "checkbox" && $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-checkbox-group v-model="form.${field}">
<el-checkbox>请选择字典生成</el-checkbox>
</el-checkbox-group>
</el-form-item>
#elseif($column.htmlType == "radio" && "" != $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-radio-group v-model="form.${field}">
<el-radio
v-for="dict in ${dictType}"
:key="dict.value"
#if($column.javaType == "Integer" || $column.javaType == "Long")
:label="parseInt(dict.value)"
#else
:label="dict.value"
#end
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
#elseif($column.htmlType == "radio" && $dictType)
<el-form-item label="${comment}" prop="${field}">
<el-radio-group v-model="form.${field}">
<el-radio label="1">请选择字典生成</el-radio>
</el-radio-group>
</el-form-item>
#elseif($column.htmlType == "datetime")
<el-form-item label="${comment}" prop="${field}">
<el-date-picker clearable
v-model="form.${field}"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择${comment}">
</el-date-picker>
</el-form-item>
#elseif($column.htmlType == "textarea")
<el-form-item label="${comment}" prop="${field}">
<el-input v-model="form.${field}" type="textarea" placeholder="请输入内容" />
</el-form-item>
#end
#end
#end
#end
#if($table.sub)
<el-divider content-position="center">${subTable.functionName}信息</el-divider>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" icon="Plus" @click="handleAdd${subClassName}">添加</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" icon="Delete" @click="handleDelete${subClassName}">删除</el-button>
</el-col>
</el-row>
<el-table :data="${subclassName}List" :row-class-name="row${subClassName}Index" @selection-change="handle${subClassName}SelectionChange" ref="${subclassName}">
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="序号" align="center" prop="index" width="50"/>
#foreach($column in $subTable.columns)
#set($javaField=$column.javaField)
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#if($column.pk || $javaField == ${subTableFkclassName})
#elseif($column.list && $column.htmlType == "input")
<el-table-column label="$comment" prop="${javaField}" width="150">
<template #default="scope">
<el-input v-model="scope.row.$javaField" placeholder="请输入$comment" />
</template>
</el-table-column>
#elseif($column.list && $column.htmlType == "datetime")
<el-table-column label="$comment" prop="${javaField}" width="240">
<template #default="scope">
<el-date-picker clearable
v-model="scope.row.$javaField"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择$comment">
</el-date-picker>
</template>
</el-table-column>
#elseif($column.list && ($column.htmlType == "select" || $column.htmlType == "radio") && "" != $column.dictType)
<el-table-column label="$comment" prop="${javaField}" width="150">
<template #default="scope">
<el-select v-model="scope.row.$javaField" placeholder="请选择$comment">
<el-option
v-for="dict in $column.dictType"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</template>
</el-table-column>
#elseif($column.list && ($column.htmlType == "select" || $column.htmlType == "radio") && "" == $column.dictType)
<el-table-column label="$comment" prop="${javaField}" width="150">
<template #default="scope">
<el-select v-model="scope.row.$javaField" placeholder="请选择$comment">
<el-option label="请选择字典生成" value="" />
</el-select>
</template>
</el-table-column>
#end
#end
</el-table>
#end
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="${BusinessName}">
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}";
const { proxy } = getCurrentInstance();
#if(${dicts} != '')
#set($dictsNoSymbol=$dicts.replace("'", ""))
const { ${dictsNoSymbol} } = proxy.useDict(${dicts});
#end
const ${businessName}List = ref([]);
#if($table.sub)
const ${subclassName}List = ref([]);
#end
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
#if($table.sub)
const checked${subClassName} = ref([]);
#end
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
#foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
const daterange${AttrName} = ref([]);
#end
#end
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
#foreach ($column in $columns)
#if($column.query)
$column.javaField: null#if($foreach.count != $columns.size()),#end
#end
#end
},
rules: {
#foreach ($column in $columns)
#if($column.required)
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
$column.javaField: [
{ required: true, message: "$comment不能为空", trigger: #if($column.htmlType == "select" || $column.htmlType == "radio")"change"#else"blur"#end }
]#if($foreach.count != $columns.size()),#end
#end
#end
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询${functionName}列表 */
function getList() {
loading.value = true;
#foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
queryParams.value.params = {};
#break
#end
#end
#foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
if (null != daterange${AttrName} && '' != daterange${AttrName}) {
queryParams.value.params["begin${AttrName}"] = daterange${AttrName}.value[0];
queryParams.value.params["end${AttrName}"] = daterange${AttrName}.value[1];
}
#end
#end
list${BusinessName}(queryParams.value).then(response => {
${businessName}List.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
// 取消按钮
function cancel() {
open.value = false;
reset();
}
// 表单重置
function reset() {
form.value = {
#foreach ($column in $columns)
#if($column.htmlType == "checkbox")
$column.javaField: []#if($foreach.count != $columns.size()),#end
#else
$column.javaField: null#if($foreach.count != $columns.size()),#end
#end
#end
};
#if($table.sub)
${subclassName}List.value = [];
#end
proxy.resetForm("${businessName}Ref");
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
#foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
#set($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
daterange${AttrName}.value = [];
#end
#end
proxy.resetForm("queryRef");
handleQuery();
}
// 多选框选中数据
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.${pkColumn.javaField});
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
function handleAdd() {
reset();
open.value = true;
title.value = "添加${functionName}";
}
/** 修改按钮操作 */
function handleUpdate(row) {
reset();
const _${pkColumn.javaField} = row.${pkColumn.javaField} || ids.value
get${BusinessName}(_${pkColumn.javaField}).then(response => {
form.value = response.data;
#foreach ($column in $columns)
#if($column.htmlType == "checkbox")
form.value.$column.javaField = form.value.${column.javaField}.split(",");
#end
#end
#if($table.sub)
${subclassName}List.value = response.data.${subclassName}List;
#end
open.value = true;
title.value = "修改${functionName}";
});
}
/** 提交按钮 */
function submitForm() {
proxy.#[[$]]#refs["${businessName}Ref"].validate(valid => {
if (valid) {
#foreach ($column in $columns)
#if($column.htmlType == "checkbox")
form.value.$column.javaField = form.value.${column.javaField}.join(",");
#end
#end
#if($table.sub)
form.value.${subclassName}List = ${subclassName}List.value;
#end
if (form.value.${pkColumn.javaField} != null) {
update${BusinessName}(form.value).then(response => {
proxy.#[[$modal]]#.msgSuccess("修改成功");
open.value = false;
getList();
});
} else {
add${BusinessName}(form.value).then(response => {
proxy.#[[$modal]]#.msgSuccess("新增成功");
open.value = false;
getList();
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const _${pkColumn.javaField}s = row.${pkColumn.javaField} || ids.value;
proxy.#[[$modal]]#.confirm('是否确认删除${functionName}编号为"' + _${pkColumn.javaField}s + '"的数据项?').then(function() {
return del${BusinessName}(_${pkColumn.javaField}s);
}).then(() => {
getList();
proxy.#[[$modal]]#.msgSuccess("删除成功");
}).catch(() => {});
}
#if($table.sub)
/** ${subTable.functionName}序号 */
function row${subClassName}Index({ row, rowIndex }) {
row.index = rowIndex + 1;
}
/** ${subTable.functionName}添加按钮操作 */
function handleAdd${subClassName}() {
let obj = {};
#foreach($column in $subTable.columns)
#if($column.pk || $column.javaField == ${subTableFkclassName})
#elseif($column.list && "" != $javaField)
obj.$column.javaField = "";
#end
#end
${subclassName}List.value.push(obj);
}
/** ${subTable.functionName}删除按钮操作 */
function handleDelete${subClassName}() {
if (checked${subClassName}.value.length == 0) {
proxy.#[[$modal]]#.msgError("请先选择要删除的${subTable.functionName}数据");
} else {
const ${subclassName}s = ${subclassName}List.value;
const checked${subClassName}s = checked${subClassName}.value;
${subclassName}List.value = ${subclassName}s.filter(function(item) {
return checked${subClassName}s.indexOf(item.index) == -1
});
}
}
/** 复选框选中数据 */
function handle${subClassName}SelectionChange(selection) {
checked${subClassName}.value = selection.map(item => item.index)
}
#end
/** 导出按钮操作 */
function handleExport() {
proxy.download('${moduleName}/${businessName}/export', {
...queryParams.value
}, `${businessName}_#[[${new Date().getTime()}]]#.xlsx`)
}
getList();
</script>

View File

@ -68,6 +68,7 @@ public class SysLogininforController extends BaseController
*/ */
@SaCheckPermission("monitor:logininfor:remove") @SaCheckPermission("monitor:logininfor:remove")
@Log(title = "登录日志", businessType = BusinessType.DELETE) @Log(title = "登录日志", businessType = BusinessType.DELETE)
@DeleteMapping("/{infoIds}")
public AjaxResult remove(@PathVariable Long[] infoIds) public AjaxResult remove(@PathVariable Long[] infoIds)
{ {
return toAjax(logininforService.deleteLogininforByIds(infoIds)); return toAjax(logininforService.deleteLogininforByIds(infoIds));

View File

@ -48,7 +48,7 @@ public class SysDeptServiceImpl implements ISysDeptService, DeptService {
@DataScope(deptAlias = "d") @DataScope(deptAlias = "d")
public List<SysDept> selectDeptList(SysDept dept) { public List<SysDept> selectDeptList(SysDept dept) {
// 只查询未禁用部门 // 只查询未禁用部门
dept.setStatus(UserConstants.DEPT_NORMAL); //dept.setStatus(UserConstants.DEPT_NORMAL);
return deptMapper.selectDeptList(dept); return deptMapper.selectDeptList(dept);
} }

View File

@ -1,17 +1,17 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = Ruoyi-Flex管理系统 VITE_APP_TITLE = Ruoyi-Flex管理系统
# 开发环境配置 # 开发环境配置
ENV = 'development' VITE_APP_ENV = 'development'
# 若依管理系统/开发环境 # 若依管理系统/开发环境
VUE_APP_BASE_API = '/dev-api' VITE_APP_BASE_API = '/dev-api'
# 监控地址 # 监控地址
VUE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/applications' VITE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/applications'
# powerjob任务调度控制台地址 # powerjob任务调度控制台地址
VUE_APP_POWERJOB_ADMIN = 'http://localhost:7700/' VITE_APP_POWERJOB_ADMIN = 'http://localhost:7700/'
# 路由懒加载 # 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true VITE_CLI_BABEL_TRANSPILE_MODULES = true

View File

@ -1,14 +1,17 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = Ruoyi-Flex管理系统 VITE_APP_TITLE = Ruoyi-Flex管理系统
# 生产环境配置 # 生产环境配置
ENV = 'production' VITE_APP_ENV = 'production'
# 若依管理系统/生产环境 # 若依管理系统/生产环境
VUE_APP_BASE_API = '/prod-api' VITE_APP_BASE_API = '/prod-api'
# 监控地址 # 监控地址
VUE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/applications' VITE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/applications'
# powerjob任务调度控制台地址 # powerjob任务调度控制台地址
VUE_APP_POWERJOB_ADMIN = 'http://localhost:7700/' VITE_APP_POWERJOB_ADMIN = 'http://localhost:7700/'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip

View File

@ -1,10 +1,11 @@
# 页面标题 # 页面标题
VUE_APP_TITLE = Ruoyi-Flex管理系统 VITE_APP_TITLE = Ruoyi-Flex管理系统
NODE_ENV = production # 生产环境配置
VITE_APP_ENV = 'staging'
# 测试环境配置 # 若依管理系统/生产环境
ENV = 'staging' VITE_APP_BASE_API = '/stage-api'
# 若依管理系统/测试环境 # 是否在打包时开启压缩,支持 gzip 和 brotli
VUE_APP_BASE_API = '/stage-api' VITE_BUILD_COMPRESS = gzip

View File

@ -1,10 +0,0 @@
# 忽略build目录下类型为js的文件的语法检查
build/*.js
# 忽略src/assets目录下文件的语法检查
src/assets
# 忽略public目录下文件的语法检查
public
# 忽略当前目录下为js的文件的语法检查
*.js
# 忽略当前目录下为vue的文件的语法检查
*.vue

View File

@ -1,199 +0,0 @@
// ESlint 检查配置
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true,
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
// add your custom rules here
//it is base on https://github.com/vuejs/eslint-config-vue
rules: {
"vue/max-attributes-per-line": [2, {
"singleline": 10,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"],
"vue/no-v-html": "off",
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ["error", "always", {"null": "ignore"}],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

View File

@ -2,22 +2,18 @@
```bash ```bash
# 克隆项目 # 克隆项目
git clone https://gitee.com/y_project/RuoYi-Vue git clone https://gitee.com/dataprince/ruoyi-flex
# 进入项目目录 # 进入项目目录
cd ruoyi-ui cd ruoyi-ui
# 安装依赖 # 安装依赖
npm install
# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npmmirror.com npm install --registry=https://registry.npmmirror.com
# 启动服务 # 启动服务
npm run dev npm run dev
```
浏览器访问 http://localhost:80 前端浏览器访问 http://localhost:80
## 发布 ## 发布

View File

@ -7,6 +7,6 @@ echo.
cd %~dp0 cd %~dp0
cd .. cd ..
npm run build:prod yarn build:prod
pause pause

View File

@ -7,6 +7,6 @@ echo.
cd %~dp0 cd %~dp0
cd .. cd ..
npm install --registry=https://registry.npmmirror.com yarn --registry=https://registry.npm.taobao.org
pause pause

View File

@ -1,12 +1,12 @@
@echo off @echo off
echo. echo.
echo [信息] 使用 Vue CLI 命令运行 Web 工程。 echo [信息] 使用 Vite 命令运行 Web 工程。
echo. echo.
%~d0 %~d0
cd %~dp0 cd %~dp0
cd .. cd ..
npm run dev yarn dev
pause pause

View File

@ -1,35 +0,0 @@
const { run } = require('runjs')
const chalk = require('chalk')
const config = require('../vue.config.js')
const rawArgv = process.argv.slice(2)
const args = rawArgv.join(' ')
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
const report = rawArgv.includes('--report')
run(`vue-cli-service build ${args}`)
const port = 9526
const publicPath = config.publicPath
var connect = require('connect')
var serveStatic = require('serve-static')
const app = connect()
app.use(
publicPath,
serveStatic('./dist', {
index: ['index.html', '/']
})
)
app.listen(port, function () {
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
if (report) {
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
}
})
} else {
run(`vue-cli-service build ${args}`)
}

View File

@ -1,14 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<meta charset="utf-8"> <head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta charset="utf-8">
<meta name="renderer" content="webkit"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <meta name="renderer" content="webkit">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title><%= webpackConfig.name %></title> <link rel="icon" href="/favicon.ico">
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]--> <title>Ruoyi-Flex管理系统</title>
<style> <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<style>
html, html,
body, body,
#app { #app {
@ -16,6 +17,7 @@
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
} }
.chromeframe { .chromeframe {
margin: 0.2em 0; margin: 0.2em 0;
background: #ccc; background: #ccc;
@ -92,6 +94,7 @@
-ms-transform: rotate(0deg); -ms-transform: rotate(0deg);
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
-webkit-transform: rotate(360deg); -webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg); -ms-transform: rotate(360deg);
@ -105,6 +108,7 @@
-ms-transform: rotate(0deg); -ms-transform: rotate(0deg);
transform: rotate(0deg); transform: rotate(0deg);
} }
100% { 100% {
-webkit-transform: rotate(360deg); -webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg); -ms-transform: rotate(360deg);
@ -194,15 +198,18 @@
opacity: 0.5; opacity: 0.5;
} }
</style> </style>
</head> </head>
<body>
<div id="app"> <body>
<div id="loader-wrapper"> <div id="app">
<div id="loader"></div> <div id="loader-wrapper">
<div class="loader-section section-left"></div> <div id="loader"></div>
<div class="loader-section section-right"></div> <div class="loader-section section-left"></div>
<div class="load_title">正在加载系统资源,请耐心等待</div> <div class="loader-section section-right"></div>
</div> <div class="load_title">正在加载系统资源,请耐心等待</div>
</div> </div>
</body> </div>
<script type="module" src="/src/main.js"></script>
</body>
</html> </html>

View File

@ -5,11 +5,10 @@
"author": "数据小王子", "author": "数据小王子",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "vue-cli-service serve", "dev": "vite serve --mode development",
"build:prod": "vue-cli-service build", "build:prod": "vite build --mode production &&vue-tsc --noEmit",
"build:stage": "vue-cli-service build --mode staging", "build:stage": "vite build --mode staging",
"preview": "node build/index.js --preview", "preview": "vite preview"
"lint": "eslint --ext .js,.vue src"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@ -36,48 +35,49 @@
"url": "https://gitee.com/dataprince/ruoyi-flex.git" "url": "https://gitee.com/dataprince/ruoyi-flex.git"
}, },
"dependencies": { "dependencies": {
"@riophae/vue-treeselect": "0.4.0", "@element-plus/icons-vue": "2.0.10",
"axios": "0.24.0", "@vueup/vue-quill": "1.1.0",
"clipboard": "2.0.8", "@vueuse/core": "9.5.0",
"core-js": "3.25.3", "@zeronejs/utils": "^1.4.0",
"axios": "0.27.2",
"echarts": "5.4.0", "echarts": "5.4.0",
"element-ui": "2.15.13", "element-plus": "2.2.27",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"fuse.js": "6.4.3", "fuse.js": "6.6.2",
"highlight.js": "9.18.5",
"js-beautify": "1.13.0",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"jsencrypt": "3.0.0-rc.1", "jsencrypt": "3.3.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"quill": "1.3.7", "vue": "3.2.45",
"screenfull": "5.0.2", "vue-cropper": "1.0.3",
"sortablejs": "1.10.2", "vue-router": "4.1.4",
"vue": "2.6.12", "vuex": "4.0.2"
"vue-count-to": "1.0.13",
"vue-cropper": "0.5.5",
"vue-meta": "2.4.0",
"vue-router": "3.4.9",
"vuedraggable": "2.24.3",
"vuex": "3.6.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "4.4.6", "@types/file-saver": "^2.0.5",
"@vue/cli-plugin-eslint": "4.4.6", "@types/js-cookie": "^3.0.2",
"@vue/cli-service": "4.4.6", "@types/node": "^18.7.15",
"babel-eslint": "10.1.0", "@types/nprogress": "^0.2.0",
"babel-plugin-dynamic-import-node": "2.3.3", "@typescript-eslint/eslint-plugin": "^5.33.1",
"chalk": "4.1.0", "@typescript-eslint/parser": "^5.33.1",
"compression-webpack-plugin": "5.0.2", "@vitejs/plugin-vue": "3.1.0",
"connect": "3.6.6", "@vue/compiler-sfc": "3.2.22",
"eslint": "7.15.0", "autoprefixer": "^10.4.8",
"eslint-plugin-vue": "7.2.0", "eslint": "^8.22.0",
"lint-staged": "10.5.3", "eslint-config-prettier": "^8.5.0",
"runjs": "4.4.2", "eslint-plugin-vue": "^9.3.0",
"sass": "1.32.13", "fast-glob": "^3.3.1",
"sass-loader": "10.1.1", "postcss": "^8.4.16",
"script-ext-html-webpack-plugin": "2.1.5", "sass": "1.56.1",
"svg-sprite-loader": "5.1.1", "tailwindcss": "^3.1.8",
"vue-template-compiler": "2.6.12" "ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"typescript": "^4.8.2",
"unplugin-auto-import": "0.11.4",
"unplugin-vue-components": "^0.22.4",
"vite": "3.2.7",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-setup-extend": "^0.4.0"
}, },
"engines": { "engines": {
"node": ">=8.9", "node": ">=8.9",

View File

@ -1,28 +1,3 @@
<template> <template>
<div id="app"> <router-view />
<router-view />
<theme-picker />
</div>
</template> </template>
<script>
import ThemePicker from "@/components/ThemePicker";
export default {
name: "App",
components: { ThemePicker },
metaInfo() {
return {
title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title,
titleTemplate: title => {
return title ? `${title} - ${process.env.VUE_APP_TITLE}` : process.env.VUE_APP_TITLE
}
}
}
};
</script>
<style scoped>
#app .theme-picker {
display: none;
}
</style>

View File

@ -1,71 +0,0 @@
import request from '@/utils/request'
// 查询定时任务调度列表
export function listJob(query) {
return request({
url: '/monitor/job/list',
method: 'get',
params: query
})
}
// 查询定时任务调度详细
export function getJob(jobId) {
return request({
url: '/monitor/job/' + jobId,
method: 'get'
})
}
// 新增定时任务调度
export function addJob(data) {
return request({
url: '/monitor/job',
method: 'post',
data: data
})
}
// 修改定时任务调度
export function updateJob(data) {
return request({
url: '/monitor/job',
method: 'put',
data: data
})
}
// 删除定时任务调度
export function delJob(jobId) {
return request({
url: '/monitor/job/' + jobId,
method: 'delete'
})
}
// 任务状态修改
export function changeJobStatus(jobId, status) {
const data = {
jobId,
status
}
return request({
url: '/monitor/job/changeStatus',
method: 'put',
data: data
})
}
// 定时任务立即执行一次
export function runJob(jobId, jobGroup) {
const data = {
jobId,
jobGroup
}
return request({
url: '/monitor/job/run',
method: 'put',
data: data
})
}

View File

@ -1,9 +0,0 @@
import request from '@/utils/request'
// 获取服务信息
export function getServer() {
return request({
url: '/monitor/server',
method: 'get'
})
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1675914273096" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2417" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M1001.7 969.6H890.4V399.4c0-27.7-17.4-52.8-43.3-62.5L580 236.7c-14.9-5.6-31.1-5.5-45.7-0.4V76.6c0-21.9-10.7-42.4-28.7-54.9s-41-15.3-61.5-7.6L176.9 114.3c-25.9 9.7-43.3 34.9-43.3 62.5v792.8H22.3C10 969.6 0 979.6 0 991.9s10 22.3 22.3 22.3H1001.8c12.3 0 22.3-10 22.3-22.3s-10.1-22.3-22.4-22.3zM178.1 176.8c0-9.2 5.8-17.6 14.4-20.8L459.7 55.8c7-2.6 14.4-1.7 20.5 2.5s9.6 10.9 9.6 18.3v893H178.1V176.8z m356.2 792.8V299.3c0-7.4 3.5-14.1 9.6-18.3 6.1-4.2 13.6-5.2 20.5-2.5l267.1 100.2c8.6 3.2 14.4 11.6 14.4 20.8v570.2H534.3z" p-id="2418"></path><path d="M391.8 346.3H258.2c-12.3 0-22.3 10-22.3 22.3s10 22.3 22.3 22.3h133.6c12.3 0 22.3-10 22.3-22.3s-10-22.3-22.3-22.3zM748 479.9H614.4c-12.3 0-22.3 10-22.3 22.3s10 22.3 22.3 22.3H748c12.3 0 22.3-10 22.3-22.3s-10-22.3-22.3-22.3zM748 613.4H614.4c-12.3 0-22.3 10-22.3 22.3s10 22.3 22.3 22.3H748c12.3 0 22.3-10 22.3-22.3s-10-22.3-22.3-22.3zM391.8 613.4H258.2c-12.3 0-22.3 10-22.3 22.3s10 22.3 22.3 22.3h133.6c12.3 0 22.3-10 22.3-22.3s-10-22.3-22.3-22.3z" p-id="2419"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1686919908144" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2521" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 992C246.895625 992 32 777.104375 32 512S246.895625 32 512 32s480 214.895625 480 480-214.895625 480-480 480z m242.9521875-533.3278125h-272.56875a23.7121875 23.7121875 0 0 0-23.71125 23.7121875l-0.024375 59.255625c0 13.08 10.6078125 23.7121875 23.6878125 23.7121875h165.96c13.104375 0 23.7121875 10.6078125 23.7121875 23.6878125v11.855625a71.1121875 71.1121875 0 0 1-71.1121875 71.1121875h-225.215625a23.7121875 23.7121875 0 0 1-23.6878125-23.7121875V423.1278125a71.1121875 71.1121875 0 0 1 71.0878125-71.1121875h331.824375a23.7121875 23.7121875 0 0 0 23.6878125-23.71125l0.0721875-59.2565625a23.7121875 23.7121875 0 0 0-23.68875-23.7121875H423.08a177.76875 177.76875 0 0 0-177.76875 177.7921875V754.953125c0 13.1034375 10.60875 23.7121875 23.713125 23.7121875h349.63125a159.984375 159.984375 0 0 0 159.984375-159.984375V482.36a23.7121875 23.7121875 0 0 0-23.7121875-23.6878125z" fill="#515151" p-id="2522"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@ -0,0 +1,65 @@
// base color
$blue: #324157;
$light-blue: #3A71A8;
$red: #C03639;
$pink: #E65D6E;
$green: #30B08F;
$tiffany: #4AB7BD;
$yellow: #FEC171;
$panGreen: #30B08F;
// 默认菜单主题风格
$base-menu-color: #bfcbd9;
$base-menu-color-active: #f4f4f5;
$base-menu-background: #304156;
$base-logo-title-color: #ffffff;
$base-menu-light-color: rgba(0, 0, 0, 0.7);
$base-menu-light-background: #ffffff;
$base-logo-light-title-color: #001529;
$base-sub-menu-background: #1f2d3d;
$base-sub-menu-hover: #001528;
// 自定义暗色菜单风格
/**
$base-menu-color:hsla(0,0%,100%,.65);
$base-menu-color-active:#fff;
$base-menu-background:#001529;
$base-logo-title-color: #ffffff;
$base-menu-light-color:rgba(0,0,0,.70);
$base-menu-light-background:#ffffff;
$base-logo-light-title-color: #001529;
$base-sub-menu-background:#000c17;
$base-sub-menu-hover:#001528;
*/
$--color-primary: #409EFF;
$--color-success: #67C23A;
$--color-warning: #E6A23C;
$--color-danger: #F56C6C;
$--color-info: #909399;
$base-sidebar-width: 200px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
menuColor: $base-menu-color;
menuLightColor: $base-menu-light-color;
menuColorActive: $base-menu-color-active;
menuBackground: $base-menu-background;
menuLightBackground: $base-menu-light-background;
subMenuBackground: $base-sub-menu-background;
subMenuHover: $base-sub-menu-hover;
sideBarWidth: $base-sidebar-width;
logoTitleColor: $base-logo-title-color;
logoLightTitleColor: $base-logo-light-title-color;
primaryColor: $--color-primary;
successColor: $--color-success;
dangerColor: $--color-danger;
infoColor: $--color-info;
warningColor: $--color-warning;
}

View File

@ -9,54 +9,46 @@
</el-breadcrumb> </el-breadcrumb>
</template> </template>
<script> <script setup>
export default { const route = useRoute();
data() { const router = useRouter();
return { const levelList = ref([])
levelList: null
}
},
watch: {
$route(route) {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) {
return
}
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) { function getBreadcrumb() {
matched = [{ path: '/index', meta: { title: '首页' }}].concat(matched) // only show routes with meta.title
} let matched = route.matched.filter(item => item.meta && item.meta.title);
const first = matched[0]
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false) //
}, if (!isDashboard(first)) {
isDashboard(route) { matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched)
const name = route && route.name
if (!name) {
return false
}
return name.trim() === 'Index'
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(path)
}
} }
levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
} }
function isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim() === 'Index'
}
function handleLink(item) {
const { redirect, path } = item
if (redirect) {
router.push(redirect)
return
}
router.push(path)
}
watchEffect(() => {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) {
return
}
getBreadcrumb()
})
getBreadcrumb();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -182,7 +182,6 @@ export default {
"updateCrontabValue", name, value, from; "updateCrontabValue", name, value, from;
this.crontabValueObj[name] = value; this.crontabValueObj[name] = value;
if (from && from !== name) { if (from && from !== name) {
console.log(`来自组件 ${from} 改变了 ${name} ${value}`);
this.changeRadio(name, value); this.changeRadio(name, value);
} }
}, },

View File

@ -3,88 +3,45 @@
<template v-for="(item, index) in options"> <template v-for="(item, index) in options">
<template v-if="values.includes(item.value)"> <template v-if="values.includes(item.value)">
<span <span
v-if="item.raw.listClass == 'default' || item.raw.listClass == ''" v-if="item.elTagType == 'default' || item.elTagType == ''"
:key="item.value" :key="item.value"
:index="index" :index="index"
:class="item.raw.cssClass" :class="item.elTagType"
>{{ item.label + " " }}</span >{{ item.label }}</span>
>
<el-tag <el-tag
v-else v-else
:disable-transitions="true" :disable-transitions="true"
:key="item.value" :key="item.value + ''"
:index="index" :index="index"
:type="item.raw.listClass == 'primary' ? '' : item.raw.listClass" :type="item.elTagType === 'primary' ? '' : item.elTagType"
:class="item.raw.cssClass" :class="item.elTagType"
> >{{ item.label }}</el-tag>
{{ item.label + " " }}
</el-tag>
</template> </template>
</template> </template>
<template v-if="unmatch && showValue">
{{ unmatchArray | handleArray }}
</template>
</div> </div>
</template> </template>
<script> <script setup>
export default { const props = defineProps({
name: "DictTag", //
props: { options: {
options: { type: Array,
type: Array, default: null,
default: null,
},
value: [Number, String, Array],
// value
showValue: {
type: Boolean,
default: true,
}
}, },
data() { //
return { value: [Number, String, Array],
unmatchArray: [], // })
}
}, const values = computed(() => {
computed: { if (props.value !== null && typeof props.value !== 'undefined') {
values() { return Array.isArray(props.value) ? props.value : [String(props.value)];
if (this.value !== null && typeof this.value !== "undefined") { } else {
return Array.isArray(this.value) ? this.value : [String(this.value)]; return [];
} else {
return [];
}
},
unmatch() {
this.unmatchArray = [];
if (this.value !== null && typeof this.value !== "undefined") {
//
if (!Array.isArray(this.value)) {
if (this.options.some((v) => v.value == this.value)) return false;
this.unmatchArray.push(this.value);
return true;
}
// Array
this.value.forEach((item) => {
if (!this.options.some((v) => v.value == item))
this.unmatchArray.push(item);
});
return true;
}
// value
return false;
},
},
filters: {
handleArray(array) {
if (array.length === 0) return "";
return array.reduce((pre, cur) => {
return pre + " " + cur;
})
}
} }
}; })
</script> </script>
<style scoped> <style scoped>
.el-tag + .el-tag { .el-tag + .el-tag {
margin-left: 10px; margin-left: 10px;

View File

@ -1,7 +1,6 @@
<template> <template>
<div class="upload-file"> <div class="upload-file">
<el-upload <el-upload
multiple
:action="uploadFileUrl" :action="uploadFileUrl"
:before-upload="handleBeforeUpload" :before-upload="handleBeforeUpload"
:file-list="fileList" :file-list="fileList"
@ -12,12 +11,12 @@
:show-file-list="false" :show-file-list="false"
:headers="headers" :headers="headers"
class="upload-file-uploader" class="upload-file-uploader"
ref="fileUpload" ref="upload"
> >
<!-- 上传按钮 --> <!-- 上传按钮 -->
<el-button size="mini" type="primary">选取文件</el-button> <el-button type="primary">选取文件</el-button>
<!-- 上传提示 --> <!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip"> <div class="el-upload__tip" v-if="showTip">
请上传 请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template> <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template> <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
@ -27,7 +26,7 @@
<!-- 文件列表 --> <!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul"> <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.url" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList"> <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank"> <el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span> <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link> </el-link>
@ -39,158 +38,132 @@
</div> </div>
</template> </template>
<script> <script setup>
import { getToken } from "@/utils/auth"; import { getToken } from "@/utils/auth";
export default { const props = defineProps({
name: "FileUpload", modelValue: [String, Object, Array],
props: { //
// limit: {
value: [String, Object, Array], type: Number,
// default: 5,
limit: { },
type: Number, // (MB)
default: 5, fileSize: {
}, type: Number,
// (MB) default: 5,
fileSize: { },
type: Number, // , ['png', 'jpg', 'jpeg']
default: 5, fileType: {
}, type: Array,
// , ['png', 'jpg', 'jpeg'] default: () => ["doc", "xls", "ppt", "txt", "pdf"],
fileType: { },
type: Array, //
default: () => ["doc", "xls", "ppt", "txt", "pdf"], isShowTip: {
}, type: Boolean,
// default: true
isShowTip: { }
type: Boolean, });
default: true
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); //
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
);
watch(() => props.modelValue, val => {
if (val) {
let temp = 1;
//
const list = Array.isArray(val) ? val : props.modelValue.split(',');
//
fileList.value = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
}
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
} else {
fileList.value = [];
return [];
}
});
//
function handleBeforeUpload(file) {
//
if (props.fileType.length) {
let fileExtension = "";
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
} }
}, const isTypeOk = props.fileType.some((type) => {
data() { if (file.type.indexOf(type) > -1) return true;
return { if (fileExtension && fileExtension.indexOf(type) > -1) return true;
number: 0, return false;
uploadList: [], });
baseUrl: process.env.VUE_APP_BASE_API, if (!isTypeOk) {
uploadFileUrl: process.env.VUE_APP_BASE_API + "/common/upload", // proxy.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
headers: { return false;
Authorization: "Bearer " + getToken(),
},
fileList: [],
};
},
watch: {
value: {
handler(val) {
if (val) {
let temp = 1;
//
const list = Array.isArray(val) ? val : this.value.split(',');
//
this.fileList = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
}
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
} else {
this.fileList = [];
return [];
}
},
deep: true,
immediate: true
}
},
computed: {
//
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
},
methods: {
//
handleBeforeUpload(file) {
//
if (this.fileType) {
const fileName = file.name.split('.');
const fileExt = fileName[fileName.length - 1];
const isTypeOk = this.fileType.indexOf(fileExt) >= 0;
if (!isTypeOk) {
this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}格式文件!`);
return false;
}
}
//
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$modal.msgError(`上传文件大小不能超过 ${this.fileSize} MB!`);
return false;
}
}
this.$modal.loading("正在上传文件,请稍候...");
this.number++;
return true;
},
//
handleExceed() {
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`);
},
//
handleUploadError(err) {
this.$modal.msgError("上传文件失败,请重试");
this.$modal.closeLoading()
},
//
handleUploadSuccess(res, file) {
if (res.code === 200) {
this.uploadList.push({ name: res.fileName, url: res.fileName });
this.uploadedSuccessfully();
} else {
this.number--;
this.$modal.closeLoading();
this.$modal.msgError(res.msg);
this.$refs.fileUpload.handleRemove(file);
this.uploadedSuccessfully();
}
},
//
handleDelete(index) {
this.fileList.splice(index, 1);
this.$emit("input", this.listToString(this.fileList));
},
//
uploadedSuccessfully() {
if (this.number > 0 && this.uploadList.length === this.number) {
this.fileList = this.fileList.concat(this.uploadList);
this.uploadList = [];
this.number = 0;
this.$emit("input", this.listToString(this.fileList));
this.$modal.closeLoading();
}
},
//
getFileName(name) {
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
} else {
return "";
}
},
//
listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
strs += list[i].url + separator;
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
} }
} }
}; //
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
return true;
}
//
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
}
//
function handleUploadError(err) {
proxy.$modal.msgError("上传失败");
}
//
function handleUploadSuccess(res, file) {
proxy.$modal.msgSuccess("上传成功");
fileList.value.push({ name: res.fileName, url: res.fileName });
emit("update:modelValue", listToString(fileList.value));
}
//
function handleDelete(index) {
fileList.value.splice(index, 1);
emit("update:modelValue", listToString(fileList.value));
}
//
function getFileName(name) {
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1).toLowerCase();
} else {
return "";
}
}
//
function listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
strs += list[i].url + separator;
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -13,20 +13,17 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { defineProps({
name: 'Hamburger', isActive: {
props: { type: Boolean,
isActive: { default: false
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
} }
})
const emit = defineEmits()
const toggleClick = () => {
emit('toggleClick');
} }
</script> </script>

View File

@ -1,8 +1,8 @@
<template> <template>
<div :class="{'show':show}" class="header-search"> <div :class="{ 'show': show }" class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" /> <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select <el-select
ref="headerSearchSelect" ref="headerSearchSelectRef"
v-model="search" v-model="search"
:remote-method="querySearch" :remote-method="querySearch"
filterable filterable
@ -17,138 +17,129 @@
</div> </div>
</template> </template>
<script> <script setup>
// fuse is a lightweight fuzzy-search module import Fuse from 'fuse.js'
// make search results more in line with expectations import { getNormalPath } from '@/utils/ruoyi'
import Fuse from 'fuse.js/dist/fuse.min.js' import { isHttp } from '@/utils/validate'
import path from 'path'
export default { const search = ref('');
name: 'HeaderSearch', const options = ref([]);
data() { const searchPool = ref([]);
return { const show = ref(false);
search: '', const fuse = ref(undefined);
options: [], const headerSearchSelectRef = ref(null);
searchPool: [], const store = useStore();
show: false, const router = useRouter();
fuse: undefined const routes = computed(() => store.getters.permission_routes);
function click() {
show.value = !show.value
if (show.value) {
headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
}
};
function close() {
headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
options.value = []
show.value = false
}
function change(val) {
const path = val.path;
if (isHttp(path)) {
// http(s)://
const pindex = path.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank");
} else {
router.push(path)
}
search.value = ''
options.value = []
nextTick(() => {
show.value = false
})
}
function initFuse(list) {
fuse.value = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [{
name: 'title',
weight: 0.7
}, {
name: 'path',
weight: 0.3
}]
})
}
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
function generateRoutes(routes, basePath = '', prefixTitle = []) {
let res = []
for (const r of routes) {
// skip hidden router
if (r.hidden) { continue }
const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
const data = {
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
title: [...prefixTitle]
} }
},
computed: { if (r.meta && r.meta.title) {
routes() { data.title = [...data.title, r.meta.title]
return this.$store.getters.permission_routes
} if (r.redirect !== 'noRedirect') {
}, // only push the routes with title
watch: { // special case: need to exclude parent router without redirect
routes() { res.push(data)
this.searchPool = this.generateRoutes(this.routes)
},
searchPool(list) {
this.initFuse(list)
},
show(value) {
if (value) {
document.body.addEventListener('click', this.close)
} else {
document.body.removeEventListener('click', this.close)
} }
} }
},
mounted() { // recursive child routes
this.searchPool = this.generateRoutes(this.routes) if (r.children) {
}, const tempRoutes = generateRoutes(r.children, data.path, data.title)
methods: { if (tempRoutes.length >= 1) {
click() { res = [...res, ...tempRoutes]
this.show = !this.show
if (this.show) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
} }
},
close() {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
this.options = []
this.show = false
},
change(val) {
const path = val.path;
if(this.ishttp(val.path)) {
// http(s)://
const pindex = path.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank");
} else {
this.$router.push(val.path)
}
this.search = ''
this.options = []
this.$nextTick(() => {
this.show = false
})
},
initFuse(list) {
this.fuse = new Fuse(list, {
shouldSort: true,
threshold: 0.4,
location: 0,
distance: 100,
minMatchCharLength: 1,
keys: [{
name: 'title',
weight: 0.7
}, {
name: 'path',
weight: 0.3
}]
})
},
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
generateRoutes(routes, basePath = '/', prefixTitle = []) {
let res = []
for (const router of routes) {
// skip hidden router
if (router.hidden) { continue }
const data = {
path: !this.ishttp(router.path) ? path.resolve(basePath, router.path) : router.path,
title: [...prefixTitle]
}
if (router.meta && router.meta.title) {
data.title = [...data.title, router.meta.title]
if (router.redirect !== 'noRedirect') {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
// recursive child routes
if (router.children) {
const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
}
}
return res
},
querySearch(query) {
if (query !== '') {
this.options = this.fuse.search(query)
} else {
this.options = []
}
},
ishttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
} }
} }
return res
} }
function querySearch(query) {
if (query !== '') {
options.value = fuse.value.search(query)
} else {
options.value = []
}
}
onMounted(() => {
searchPool.value = generateRoutes(routes.value);
})
watchEffect(() => {
searchPool.value = generateRoutes(routes.value)
})
watch(show, (value) => {
if (value) {
document.body.addEventListener('click', close)
} else {
document.body.removeEventListener('click', close)
}
})
watch(searchPool, (list) => {
initFuse(list)
})
</script> </script>
<style lang="scss" scoped> <style lang='scss' scoped>
.header-search { .header-search {
font-size: 0 !important; font-size: 0 !important;
@ -168,7 +159,7 @@ export default {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
::v-deep .el-input__inner { :deep(.el-input__inner) {
border-radius: 0; border-radius: 0;
border: 0; border: 0;
padding-left: 0; padding-left: 0;

View File

@ -1,104 +1,74 @@
<!-- @author zhengjie -->
<template> <template>
<div class="icon-body"> <div class="icon-body">
<el-input v-model="name" class="icon-search" clearable placeholder="请输入图标名称" @clear="filterIcons" @input="filterIcons"> <el-input
<i slot="suffix" class="el-icon-search el-input__icon" /> v-model="iconName"
style="position: relative;"
clearable
placeholder="请输入图标名称"
@clear="filterIcons"
@input="filterIcons"
>
<template #suffix><i class="el-icon-search el-input__icon" /></template>
</el-input> </el-input>
<div class="icon-list"> <div class="icon-list">
<div class="list-container"> <div v-for="(item, index) in iconList" :key="index" @click="selectedIcon(item)">
<div v-for="(item, index) in iconList" class="icon-item-wrapper" :key="index" @click="selectedIcon(item)"> <svg-icon :icon-class="item" style="height: 30px;width: 16px;" />
<div :class="['icon-item', { active: activeIcon === item }]"> <span>{{ item }}</span>
<svg-icon :icon-class="item" class-name="icon" style="height: 25px;width: 16px;"/>
<span>{{ item }}</span>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup>
import icons from './requireIcons' import icons from './requireIcons'
export default {
name: 'IconSelect', const iconName = ref('');
props: { const iconList = ref(icons);
activeIcon: { const emit = defineEmits(['selected']);
type: String
function filterIcons() {
iconList.value = icons
if (iconName.value) {
iconList.value = icons.filter(item => item.indexOf(iconName.value) !== -1)
}
}
function selectedIcon(name) {
emit('selected', name)
document.body.click()
}
function reset() {
iconName.value = ''
iconList.value = icons
}
defineExpose({
reset
})
</script>
<style lang='scss' scoped>
.icon-body {
width: 100%;
padding: 10px;
.icon-list {
height: 200px;
overflow-y: scroll;
div {
height: 30px;
line-height: 30px;
margin-bottom: -5px;
cursor: pointer;
width: 33%;
float: left;
} }
}, span {
data() { display: inline-block;
return { vertical-align: -0.15em;
name: '', fill: currentColor;
iconList: icons overflow: hidden;
}
},
methods: {
filterIcons() {
this.iconList = icons
if (this.name) {
this.iconList = this.iconList.filter(item => item.includes(this.name))
}
},
selectedIcon(name) {
this.$emit('selected', name)
document.body.click()
},
reset() {
this.name = ''
this.iconList = icons
} }
} }
} }
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.icon-body {
width: 100%;
padding: 10px;
.icon-search {
position: relative;
margin-bottom: 5px;
}
.icon-list {
height: 200px;
overflow: auto;
.list-container {
display: flex;
flex-wrap: wrap;
.icon-item-wrapper {
width: calc(100% / 3);
height: 25px;
line-height: 25px;
cursor: pointer;
display: flex;
.icon-item {
display: flex;
max-width: 100%;
height: 100%;
padding: 0 5px;
&:hover {
background: #ececec;
border-radius: 5px;
}
.icon {
flex-shrink: 0;
}
span {
display: inline-block;
vertical-align: -0.15em;
fill: currentColor;
padding-left: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.icon-item.active {
background: #ececec;
border-radius: 5px;
}
}
}
}
}
</style> </style>

View File

@ -1,11 +1,8 @@
let icons = []
const req = require.context('../../assets/icons/svg', false, /\.svg$/) const modules = import.meta.glob('./../../assets/icons/svg/*.svg');
const requireAll = requireContext => requireContext.keys() for (const path in modules) {
const p = path.split('assets/icons/svg/')[1].split('.svg')[0];
const re = /\.\/(.*)\.svg/ icons.push(p);
}
const icons = requireAll(req).map(i => {
return i.match(re)[1]
})
export default icons export default icons

View File

@ -5,64 +5,59 @@
:style="`width:${realWidth};height:${realHeight};`" :style="`width:${realWidth};height:${realHeight};`"
:preview-src-list="realSrcList" :preview-src-list="realSrcList"
> >
<div slot="error" class="image-slot"> <template #error>
<i class="el-icon-picture-outline"></i> <div class="image-slot">
</div> <el-icon><picture-filled /></el-icon>
</div>
</template>
</el-image> </el-image>
</template> </template>
<script> <script setup>
import { isExternal } from "@/utils/validate"; import { isExternal } from "@/utils/validate";
export default { const props = defineProps({
name: "ImagePreview", src: {
props: { type: String,
src: { required: true
type: String,
default: ""
},
width: {
type: [Number, String],
default: ""
},
height: {
type: [Number, String],
default: ""
}
}, },
computed: { width: {
realSrc() { type: [Number, String],
if (!this.src) { default: ""
return;
}
let real_src = this.src.split(",")[0];
if (isExternal(real_src)) {
return real_src;
}
return process.env.VUE_APP_BASE_API + real_src;
},
realSrcList() {
if (!this.src) {
return;
}
let real_src_list = this.src.split(",");
let srcList = [];
real_src_list.forEach(item => {
if (isExternal(item)) {
return srcList.push(item);
}
return srcList.push(process.env.VUE_APP_BASE_API + item);
});
return srcList;
},
realWidth() {
return typeof this.width == "string" ? this.width : `${this.width}px`;
},
realHeight() {
return typeof this.height == "string" ? this.height : `${this.height}px`;
}
}, },
}; height: {
type: [Number, String],
default: ""
}
});
const realSrc = computed(() => {
let real_src = props.src.split(",")[0];
if (isExternal(real_src)) {
return real_src;
}
return import.meta.env.VITE_APP_BASE_API + real_src;
});
const realSrcList = computed(() => {
let real_src_list = props.src.split(",");
let srcList = [];
real_src_list.forEach(item => {
if (isExternal(item)) {
return srcList.push(item);
}
return srcList.push(import.meta.env.VITE_APP_BASE_API + item);
});
return srcList;
});
const realWidth = computed(() =>
typeof props.width == "string" ? props.width : `${props.width}px`
);
const realHeight = computed(() =>
typeof props.height == "string" ? props.height : `${props.height}px`
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -70,14 +65,14 @@ export default {
border-radius: 5px; border-radius: 5px;
background-color: #ebeef5; background-color: #ebeef5;
box-shadow: 0 0 5px 1px #ccc; box-shadow: 0 0 5px 1px #ccc;
::v-deep .el-image__inner { :deep(.el-image__inner) {
transition: all 0.3s; transition: all 0.3s;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
transform: scale(1.2); transform: scale(1.2);
} }
} }
::v-deep .image-slot { :deep(.image-slot) {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View File

@ -1,7 +1,6 @@
<template> <template>
<div class="component-upload-image"> <div class="component-upload-image">
<el-upload <el-upload
multiple
:action="uploadImgUrl" :action="uploadImgUrl"
list-type="picture-card" list-type="picture-card"
:on-success="handleUploadSuccess" :on-success="handleUploadSuccess"
@ -9,29 +8,32 @@
:limit="limit" :limit="limit"
:on-error="handleUploadError" :on-error="handleUploadError"
:on-exceed="handleExceed" :on-exceed="handleExceed"
ref="imageUpload" name="file"
:on-remove="handleDelete" :on-remove="handleRemove"
:show-file-list="true" :show-file-list="true"
:headers="headers" :headers="headers"
:file-list="fileList" :file-list="fileList"
:on-preview="handlePictureCardPreview" :on-preview="handlePictureCardPreview"
:class="{hide: this.fileList.length >= this.limit}" :class="{ hide: fileList.length >= limit }"
> >
<i class="el-icon-plus"></i> <el-icon class="avatar-uploader-icon"><plus /></el-icon>
</el-upload> </el-upload>
<!-- 上传提示 --> <!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip"> <div class="el-upload__tip" v-if="showTip">
请上传 请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template> <template v-if="fileSize">
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
</template>
的文件 的文件
</div> </div>
<el-dialog <el-dialog
:visible.sync="dialogVisible" v-model="dialogVisible"
title="预览" title="预览"
width="800" width="800px"
append-to-body append-to-body
> >
<img <img
@ -42,185 +44,154 @@
</div> </div>
</template> </template>
<script> <script setup>
import { getToken } from "@/utils/auth"; import { getToken } from "@/utils/auth";
export default { const props = defineProps({
props: { modelValue: [String, Object, Array],
value: [String, Object, Array], //
// limit: {
limit: { type: Number,
type: Number, default: 5,
default: 5,
},
// (MB)
fileSize: {
type: Number,
default: 5,
},
// , ['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["png", "jpg", "jpeg"],
},
//
isShowTip: {
type: Boolean,
default: true
}
}, },
data() { // (MB)
return { fileSize: {
number: 0, type: Number,
uploadList: [], default: 5,
dialogImageUrl: "",
dialogVisible: false,
hideUpload: false,
baseUrl: process.env.VUE_APP_BASE_API,
uploadImgUrl: process.env.VUE_APP_BASE_API + "/common/upload", //
headers: {
Authorization: "Bearer " + getToken(),
},
fileList: []
};
}, },
watch: { // , ['png', 'jpg', 'jpeg']
value: { fileType: {
handler(val) { type: Array,
if (val) { default: () => ["png", "jpg", "jpeg"],
//
const list = Array.isArray(val) ? val : this.value.split(',');
//
this.fileList = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(this.baseUrl) === -1) {
item = { name: this.baseUrl + item, url: this.baseUrl + item };
} else {
item = { name: item, url: item };
}
}
return item;
});
} else {
this.fileList = [];
return [];
}
},
deep: true,
immediate: true
}
}, },
computed: { //
// isShowTip: {
showTip() { type: Boolean,
return this.isShowTip && (this.fileType || this.fileSize); default: true
},
}, },
methods: { });
// loading
handleBeforeUpload(file) {
let isImg = false;
if (this.fileType.length) {
let fileExtension = "";
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
}
isImg = this.fileType.some(type => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
} else {
isImg = file.type.indexOf("image") > -1;
}
if (!isImg) { const { proxy } = getCurrentInstance();
this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`); const emit = defineEmits();
return false; const dialogImageUrl = ref("");
} const dialogVisible = ref(false);
if (this.fileSize) { const baseUrl = import.meta.env.VITE_APP_BASE_API;
const isLt = file.size / 1024 / 1024 < this.fileSize; const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); //
if (!isLt) { const headers = ref({ Authorization: "Bearer " + getToken() });
this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`); const fileList = ref([]);
return false; const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
);
watch(() => props.modelValue, val => {
if (val) {
//
const list = Array.isArray(val) ? val : props.modelValue.split(",");
//
fileList.value = list.map(item => {
if (typeof item === "string") {
if (item.indexOf(baseUrl) === -1) {
item = { name: baseUrl + item, url: baseUrl + item };
} else {
item = { name: item, url: item };
} }
} }
this.$modal.loading("正在上传图片,请稍候..."); return item;
this.number++; });
}, } else {
// fileList.value = [];
handleExceed() { return [];
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`); }
}, });
//
handleUploadSuccess(res, file) { //
if (res.code === 200) { function handleRemove(file, files) {
this.uploadList.push({ name: res.fileName, url: res.fileName }); const findex = fileList.value.map(f => f.name).indexOf(file.name);
this.uploadedSuccessfully(); if (findex > -1) {
} else { fileList.value.splice(findex, 1);
this.number--; emit("update:modelValue", listToString(fileList.value));
this.$modal.closeLoading(); }
this.$modal.msgError(res.msg); }
this.$refs.imageUpload.handleRemove(file);
this.uploadedSuccessfully(); //
} function handleUploadSuccess(res) {
}, fileList.value.push({ name: res.fileName, url: res.fileName });
// emit("update:modelValue", listToString(fileList.value));
handleDelete(file) { proxy.$modal.closeLoading();
const findex = this.fileList.map(f => f.name).indexOf(file.name); }
if(findex > -1) {
this.fileList.splice(findex, 1); // loading
this.$emit("input", this.listToString(this.fileList)); function handleBeforeUpload(file) {
} let isImg = false;
}, if (props.fileType.length) {
// let fileExtension = "";
handleUploadError() { if (file.name.lastIndexOf(".") > -1) {
this.$modal.msgError("上传图片失败,请重试"); fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
this.$modal.closeLoading(); }
}, isImg = props.fileType.some(type => {
// if (file.type.indexOf(type) > -1) return true;
uploadedSuccessfully() { if (fileExtension && fileExtension.indexOf(type) > -1) return true;
if (this.number > 0 && this.uploadList.length === this.number) { return false;
this.fileList = this.fileList.concat(this.uploadList); });
this.uploadList = []; } else {
this.number = 0; isImg = file.type.indexOf("image") > -1;
this.$emit("input", this.listToString(this.fileList)); }
this.$modal.closeLoading(); if (!isImg) {
} proxy.$modal.msgError(
}, `文件格式不正确, 请上传${props.fileType.join("/")}图片格式文件!`
// );
handlePictureCardPreview(file) { return false;
this.dialogImageUrl = file.url; }
this.dialogVisible = true; if (props.fileSize) {
}, const isLt = file.size / 1024 / 1024 < props.fileSize;
// if (!isLt) {
listToString(list, separator) { proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
let strs = ""; return false;
separator = separator || ",";
for (let i in list) {
if (list[i].url) {
strs += list[i].url.replace(this.baseUrl, "") + separator;
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
} }
} }
}; proxy.$modal.loading("上传中");
}
//
function handleExceed() {
proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
}
//
function handleUploadError() {
proxy.$modal.msgError("上传失败");
proxy.$modal.closeLoading();
}
//
function handlePictureCardPreview(file) {
dialogImageUrl.value = file.url;
dialogVisible.value = true;
}
//
function listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
strs += list[i].url.replace(baseUrl, "") + separator;
}
return strs != "" ? strs.substr(0, strs.length - 1) : "";
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
// .el-upload--picture-card // .el-upload--picture-card
::v-deep.hide .el-upload--picture-card { :deep(.hide .el-upload--picture-card) {
display: none; display: none;
} }
// //
::v-deep .el-list-enter-active, :deep(.el-list-enter-active),
::v-deep .el-list-leave-active { :deep(.el-list-leave-active) {
transition: all 0s; transition: all 0s;
} }
:deep(.el-list-enter, .el-list-leave-active) {
::v-deep .el-list-enter, .el-list-leave-active { opacity: 0;
opacity: 0; transform: translateY(0);
transform: translateY(0);
} }
</style> </style>

View File

@ -1,106 +1,94 @@
<template> <template>
<div :class="{'hidden':hidden}" class="pagination-container"> <div :class="{ 'hidden': hidden }" class="pagination-container">
<el-pagination <el-pagination
:background="background" :background="background"
:current-page.sync="currentPage" v-model:current-page="currentPage"
:page-size.sync="pageSize" v-model:page-size="pageSize"
:layout="layout" :layout="layout"
:page-sizes="pageSizes" :page-sizes="pageSizes"
:pager-count="pagerCount" :pager-count="pagerCount"
:total="total" :total="total"
v-bind="$attrs"
@size-change="handleSizeChange" @size-change="handleSizeChange"
@current-change="handleCurrentChange" @current-change="handleCurrentChange"
/> />
</div> </div>
</template> </template>
<script> <script setup>
import { scrollTo } from '@/utils/scroll-to' import { scrollTo } from '@/utils/scroll-to'
export default { const props = defineProps({
name: 'Pagination', total: {
props: { required: true,
total: { type: Number
required: true, },
type: Number page: {
}, type: Number,
page: { default: 1
type: Number, },
default: 1 limit: {
}, type: Number,
limit: { default: 20
type: Number, },
default: 20 pageSizes: {
}, type: Array,
pageSizes: { default() {
type: Array, return [10, 20, 30, 50]
default() {
return [10, 20, 30, 50]
}
},
// 5
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
autoScroll: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
} }
}, },
data() { // 5
return { pagerCount: {
}; type: Number,
default: document.body.clientWidth < 992 ? 5 : 7
}, },
computed: { layout: {
currentPage: { type: String,
get() { default: 'total, sizes, prev, pager, next, jumper'
return this.page
},
set(val) {
this.$emit('update:page', val)
}
},
pageSize: {
get() {
return this.limit
},
set(val) {
this.$emit('update:limit', val)
}
}
}, },
methods: { background: {
handleSizeChange(val) { type: Boolean,
if (this.currentPage * val > this.total) { default: true
this.currentPage = 1 },
} autoScroll: {
this.$emit('pagination', { page: this.currentPage, limit: val }) type: Boolean,
if (this.autoScroll) { default: true
scrollTo(0, 800) },
} hidden: {
}, type: Boolean,
handleCurrentChange(val) { default: false
this.$emit('pagination', { page: val, limit: this.pageSize }) }
if (this.autoScroll) { })
scrollTo(0, 800)
} const emit = defineEmits();
} const currentPage = computed({
get() {
return props.page
},
set(val) {
emit('update:page', val)
}
})
const pageSize = computed({
get() {
return props.limit
},
set(val){
emit('update:limit', val)
}
})
function handleSizeChange(val) {
emit('pagination', { page: currentPage.value, limit: val })
if (props.autoScroll) {
scrollTo(0, 800)
} }
} }
function handleCurrentChange(val) {
emit('pagination', { page: val, limit: pageSize.value })
if (props.autoScroll) {
scrollTo(0, 800)
}
}
</script> </script>
<style scoped> <style scoped>

View File

@ -1,17 +1,17 @@
<template> <template>
<div class="top-right-btn" :style="style"> <div class="top-right-btn">
<el-row> <el-row>
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search"> <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top">
<el-button size="mini" circle icon="el-icon-search" @click="toggleSearch()" /> <el-button circle icon="Search" @click="toggleSearch()" />
</el-tooltip> </el-tooltip>
<el-tooltip class="item" effect="dark" content="刷新" placement="top"> <el-tooltip class="item" effect="dark" content="刷新" placement="top">
<el-button size="mini" circle icon="el-icon-refresh" @click="refresh()" /> <el-button circle icon="Refresh" @click="refresh()" />
</el-tooltip> </el-tooltip>
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns"> <el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
<el-button size="mini" circle icon="el-icon-menu" @click="showColumn()" /> <el-button circle icon="Menu" @click="showColumn()" />
</el-tooltip> </el-tooltip>
</el-row> </el-row>
<el-dialog :title="title" :visible.sync="open" append-to-body> <el-dialog :title="title" v-model="open" append-to-body>
<el-transfer <el-transfer
:titles="['显示', '隐藏']" :titles="['显示', '隐藏']"
v-model="value" v-model="value"
@ -21,84 +21,69 @@
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script>
export default { <script setup>
name: "RightToolbar", const props = defineProps({
data() { showSearch: {
return { type: Boolean,
// default: true,
value: [],
//
title: "显示/隐藏",
//
open: false,
};
}, },
props: { columns: {
showSearch: { type: Array,
type: Boolean,
default: true,
},
columns: {
type: Array,
},
search: {
type: Boolean,
default: true,
},
gutter: {
type: Number,
default: 10,
},
}, },
computed: { })
style() {
const ret = {}; const emits = defineEmits(['update:showSearch', 'queryTable']);
if (this.gutter) {
ret.marginRight = `${this.gutter / 2}px`; //
} const value = ref([]);
return ret; //
} const title = ref("显示/隐藏");
}, //
created() { const open = ref(false);
//
for (let item in this.columns) { //
if (this.columns[item].visible === false) { function toggleSearch() {
this.value.push(parseInt(item)); emits("update:showSearch", !props.showSearch);
} }
}
}, //
methods: { function refresh() {
// emits("queryTable");
toggleSearch() { }
this.$emit("update:showSearch", !this.showSearch);
}, //
// function dataChange(data) {
refresh() { for (let item in props.columns) {
this.$emit("queryTable"); const key = props.columns[item].key;
}, props.columns[item].visible = !data.includes(key);
// }
dataChange(data) { }
for (let item in this.columns) {
const key = this.columns[item].key; // dialog
this.columns[item].visible = !data.includes(key); function showColumn() {
} open.value = true;
}, }
// dialog
showColumn() { //
this.open = true; for (let item in props.columns) {
}, if (props.columns[item].visible === false) {
}, value.value.push(parseInt(item));
}; }
}
</script> </script>
<style lang="scss" scoped>
::v-deep .el-transfer__button { <style lang='scss' scoped>
:deep(.el-transfer__button) {
border-radius: 50%; border-radius: 50%;
padding: 12px;
display: block; display: block;
margin-left: 0px; margin-left: 0px;
} }
::v-deep .el-transfer__button:first-child { :deep(.el-transfer__button:first-child) {
margin-bottom: 10px; margin-bottom: 10px;
} }
.my-el-transfer {
text-align: center;
}
</style> </style>

View File

@ -4,18 +4,10 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { const url = ref('http://doc.ruoyi.vip/ruoyi-vue');
name: 'RuoYiDoc',
data() { function goto() {
return { window.open(url.value)
url: 'http://doc.ruoyi.vip/ruoyi-vue'
}
},
methods: {
goto() {
window.open(this.url)
}
}
} }
</script> </script>

View File

@ -4,18 +4,10 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { const url = ref('https://gitee.com/y_project/RuoYi-Vue');
name: 'RuoYiGit',
data() { function goto() {
return { window.open(url.value)
url: 'https://gitee.com/y_project/RuoYi-Vue'
}
},
methods: {
goto() {
window.open(this.url)
}
}
} }
</script> </script>

View File

@ -1,55 +1,20 @@
<template> <template>
<div> <div>
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" /> <svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
</div> </div>
</template> </template>
<script> <script setup>
import screenfull from 'screenfull' import { useFullscreen } from '@vueuse/core'
export default { const { isFullscreen, enter, exit, toggle } = useFullscreen();
name: 'Screenfull',
data() {
return {
isFullscreen: false
}
},
mounted() {
this.init()
},
beforeDestroy() {
this.destroy()
},
methods: {
click() {
if (!screenfull.isEnabled) {
this.$message({ message: '你的浏览器不支持全屏', type: 'warning' })
return false
}
screenfull.toggle()
},
change() {
this.isFullscreen = screenfull.isFullscreen
},
init() {
if (screenfull.isEnabled) {
screenfull.on('change', this.change)
}
},
destroy() {
if (screenfull.isEnabled) {
screenfull.off('change', this.change)
}
}
}
}
</script> </script>
<style scoped> <style lang='scss' scoped>
.screenfull-svg { .screenfull-svg {
display: inline-block; display: inline-block;
cursor: pointer; cursor: pointer;
fill: #5a5e66;; fill: #5a5e66;
width: 20px; width: 20px;
height: 20px; height: 20px;
vertical-align: 10px; vertical-align: 10px;

View File

@ -1,56 +1,57 @@
<template> <template>
<el-dropdown trigger="click" @command="handleSetSize"> <div>
<div> <el-dropdown trigger="click" @command="handleSetSize">
<svg-icon class-name="size-icon" icon-class="size" /> <div class="size-icon--style">
</div> <svg-icon class-name="size-icon" icon-class="size" />
<el-dropdown-menu slot="dropdown"> </div>
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value"> <template #dropdown>
{{ item.label }} <el-dropdown-menu>
</el-dropdown-item> <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
</el-dropdown-menu> {{ item.label }}
</el-dropdown> </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template> </template>
<script> <script setup>
export default { import { ElMessage } from 'element-plus'
data() {
return {
sizeOptions: [
{ label: 'Default', value: 'default' },
{ label: 'Medium', value: 'medium' },
{ label: 'Small', value: 'small' },
{ label: 'Mini', value: 'mini' }
]
}
},
computed: {
size() {
return this.$store.getters.size
}
},
methods: {
handleSetSize(size) {
this.$ELEMENT.size = size
this.$store.dispatch('app/setSize', size)
this.refreshView()
this.$message({
message: 'Switch Size Success',
type: 'success'
})
},
refreshView() {
// In order to make the cached page re-rendered
this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
const { fullPath } = this.$route const store = useStore();
const size = computed(() => store.getters.size);
const route = useRoute();
const router = useRouter();
const {proxy} = getCurrentInstance();
const sizeOptions = ref([
{ label: '较大', value: 'large' },
{ label: '默认', value: 'default' },
{ label: '稍小', value: 'small' },
])
this.$nextTick(() => { function refreshView() {
this.$router.replace({ // In order to make the cached page re-rendered
path: '/redirect' + fullPath store.dispatch('tagsView/delAllCachedViews', route)
})
})
}
}
const { fullPath } = route
nextTick(() => {
router.replace({
path: '/redirect' + fullPath
})
})
} }
function handleSetSize(size) {
proxy.$modal.loading("正在设置布局大小,请稍候...");
store.dispatch('app/setSize', size)
setTimeout("window.location.reload()", 1000)
};
</script> </script>
<style lang='scss' scoped>
.size-icon--style {
font-size: 18px;
line-height: 50px;
padding-right: 7px;
}
</style>

View File

@ -1,15 +1,11 @@
<template> <template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" /> <svg :class="svgClass" aria-hidden="true">
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners"> <use :xlink:href="iconName" :fill="color" />
<use :xlink:href="iconName" />
</svg> </svg>
</template> </template>
<script> <script>
import { isExternal } from '@/utils/validate' export default defineComponent({
export default {
name: 'SvgIcon',
props: { props: {
iconClass: { iconClass: {
type: String, type: String,
@ -18,44 +14,40 @@ export default {
className: { className: {
type: String, type: String,
default: '' default: ''
} },
color: {
type: String,
default: ''
},
}, },
computed: { setup(props) {
isExternal() { return {
return isExternal(this.iconClass) iconName: computed(() => `#icon-${props.iconClass}`),
}, svgClass: computed(() => {
iconName() { if (props.className) {
return `#icon-${this.iconClass}` return `svg-icon ${props.className}`
}, }
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon' return 'svg-icon'
} })
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
} }
} }
} })
</script> </script>
<style scoped> <style scope lang="scss">
.sub-el-icon,
.nav-icon {
display: inline-block;
font-size: 15px;
margin-right: 12px;
position: relative;
}
.svg-icon { .svg-icon {
width: 1em; width: 1em;
height: 1em; height: 1em;
vertical-align: -0.15em; position: relative;
fill: currentColor; fill: currentColor;
overflow: hidden; vertical-align: -2px;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
} }
</style> </style>

View File

@ -0,0 +1,10 @@
import * as components from '@element-plus/icons-vue'
export default {
install: (app) => {
for (const key in components) {
const componentConfig = components[key];
app.component(componentConfig.name, componentConfig);
}
},
};

View File

@ -6,162 +6,157 @@
> >
<template v-for="(item, index) in topMenus"> <template v-for="(item, index) in topMenus">
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber" <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber"
><svg-icon :icon-class="item.meta.icon" /> ><svg-icon :icon-class="item.meta.icon" />
{{ item.meta.title }}</el-menu-item {{ item.meta.title }}</el-menu-item
> >
</template> </template>
<!-- 顶部菜单超出数量折叠 --> <!-- 顶部菜单超出数量折叠 -->
<el-submenu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber"> <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
<template slot="title">更多菜单</template> <template #title>更多菜单</template>
<template v-for="(item, index) in topMenus"> <template v-for="(item, index) in topMenus">
<el-menu-item <el-menu-item
:index="item.path" :index="item.path"
:key="index" :key="index"
v-if="index >= visibleNumber" v-if="index >= visibleNumber"
><svg-icon :icon-class="item.meta.icon" /> ><svg-icon :icon-class="item.meta.icon" />
{{ item.meta.title }}</el-menu-item {{ item.meta.title }}</el-menu-item
> >
</template> </template>
</el-submenu> </el-sub-menu>
</el-menu> </el-menu>
</template> </template>
<script> <script setup>
import { constantRoutes } from "@/router"; import { constantRoutes } from "@/router"
import { isHttp } from '@/utils/validate'
// //
const hideList = ['/index', '/user/profile']; const visibleNumber = ref(null);
//
const isFrist = ref(null);
// index
const currentIndex = ref(null);
export default { const store = useStore();
data() { const route = useRoute();
return {
// //
visibleNumber: 5, const theme = computed(() => store.state.settings.theme);
// index //
currentIndex: undefined const routers = computed(() => store.state.permission.topbarRouters);
};
}, //
computed: { const topMenus = computed(() => {
theme() { let topMenus = [];
return this.$store.state.settings.theme; routers.value.map((menu) => {
}, if (menu.hidden !== true) {
// //
topMenus() { if (menu.path === "/") {
let topMenus = []; topMenus.push(menu.children[0]);
this.routers.map((menu) => {
if (menu.hidden !== true) {
//
if (menu.path === "/") {
topMenus.push(menu.children[0]);
} else {
topMenus.push(menu);
}
}
});
return topMenus;
},
//
routers() {
return this.$store.state.permission.topbarRouters;
},
//
childrenMenus() {
var childrenMenus = [];
this.routers.map((router) => {
for (var item in router.children) {
if (router.children[item].parentPath === undefined) {
if(router.path === "/") {
router.children[item].path = "/" + router.children[item].path;
} else {
if(!this.ishttp(router.children[item].path)) {
router.children[item].path = router.path + "/" + router.children[item].path;
}
}
router.children[item].parentPath = router.path;
}
childrenMenus.push(router.children[item]);
}
});
return constantRoutes.concat(childrenMenus);
},
//
activeMenu() {
const path = this.$route.path;
let activePath = path;
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length);
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
if (!this.$route.meta.link) {
this.$store.dispatch('app/toggleSideBarHide', false);
}
} else if(!this.$route.children) {
activePath = path;
this.$store.dispatch('app/toggleSideBarHide', true);
}
this.activeRoutes(activePath);
return activePath;
},
},
beforeMount() {
window.addEventListener('resize', this.setVisibleNumber)
},
beforeDestroy() {
window.removeEventListener('resize', this.setVisibleNumber)
},
mounted() {
this.setVisibleNumber();
},
methods: {
//
setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3;
this.visibleNumber = parseInt(width / 85);
},
//
handleSelect(key, keyPath) {
this.currentIndex = key;
const route = this.routers.find(item => item.path === key);
if (this.ishttp(key)) {
// http(s)://
window.open(key, "_blank");
} else if (!route || !route.children) {
//
const routeMenu = this.childrenMenus.find(item => item.path === key);
if (routeMenu && routeMenu.query) {
let query = JSON.parse(routeMenu.query);
this.$router.push({ path: key, query: query });
} else {
this.$router.push({ path: key });
}
this.$store.dispatch('app/toggleSideBarHide', true);
} else { } else {
// topMenus.push(menu);
this.activeRoutes(key);
this.$store.dispatch('app/toggleSideBarHide', false);
} }
},
//
activeRoutes(key) {
var routes = [];
if (this.childrenMenus && this.childrenMenus.length > 0) {
this.childrenMenus.map((item) => {
if (key == item.parentPath || (key == "index" && "" == item.path)) {
routes.push(item);
}
});
}
if(routes.length > 0) {
this.$store.commit("SET_SIDEBAR_ROUTERS", routes);
} else {
this.$store.dispatch('app/toggleSideBarHide', true);
}
},
ishttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
} }
}, })
}; return topMenus;
})
//
const childrenMenus = computed(() => {
let childrenMenus = [];
routers.value.map((router) => {
for (let item in router.children) {
if (router.children[item].parentPath === undefined) {
if(router.path === "/") {
router.children[item].path = "/redirect/" + router.children[item].path;
} else {
if(!isHttp(router.children[item].path)) {
router.children[item].path = router.path + "/" + router.children[item].path;
}
}
router.children[item].parentPath = router.path;
}
childrenMenus.push(router.children[item]);
}
})
return constantRoutes.concat(childrenMenus);
})
//
const activeMenu = computed(() => {
const path = route.path;
let activePath = defaultRouter.value;
if (path !== undefined && path.lastIndexOf("/") > 0) {
const tmpPath = path.substring(1, path.length);
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
} else if ("/index" == path || "" == path) {
if (!isFrist.value) {
isFrist.value = true;
} else {
activePath = "index";
}
}
let routes = activeRoutes(activePath);
if (routes.length === 0) {
activePath = currentIndex.value || defaultRouter.value
activeRoutes(activePath);
}
return activePath;
})
//
const defaultRouter = computed(() => {
let router;
Object.keys(routers.value).some((key) => {
if (!routers.value[key].hidden) {
router = routers.value[key].path;
return true;
}
});
return router;
})
function setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3;
visibleNumber.value = parseInt(width / 85);
}
function handleSelect(key, keyPath) {
currentIndex.value = key;
if (isHttp(key)) {
// http(s)://
window.open(key, "_blank");
} else if (key.indexOf("/redirect") !== -1) {
// /redirect
router.push({ path: key.replace("/redirect", "") });
} else {
//
activeRoutes(key);
}
}
function activeRoutes(key) {
let routes = [];
if (childrenMenus.value && childrenMenus.value.length > 0) {
childrenMenus.value.map((item) => {
if (key == item.parentPath || (key == "index" && "" == item.path)) {
routes.push(item);
}
});
}
if(routes.length > 0) {
store.commit("SET_SIDEBAR_ROUTERS", routes);
}
return routes;
}
onMounted(() => {
window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber)
})
onMounted(() => {
setVisibleNumber()
})
</script> </script>
<style lang="scss"> <style lang="scss">
@ -174,13 +169,13 @@ export default {
margin: 0 10px !important; margin: 0 10px !important;
} }
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-submenu.is-active .el-submenu__title { .topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
border-bottom: 2px solid #{'var(--theme)'} !important; border-bottom: 2px solid #{'var(--theme)'} !important;
color: #303133; color: #303133;
} }
/* submenu item */ /* sub-menu item */
.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title { .topmenu-container.el-menu--horizontal > .el-sub-menu .el-submenu__title {
float: left; float: left;
height: 50px !important; height: 50px !important;
line-height: 50px !important; line-height: 50px !important;

View File

@ -0,0 +1,156 @@
<template>
<div class="el-tree-select">
<el-select
style="width: 100%"
v-model="valueId"
ref="treeSelect"
:filterable="true"
:clearable="true"
@clear="clearHandle"
:filter-method="selectFilterData"
:placeholder="placeholder"
>
<el-option :value="valueId" :label="valueTitle">
<el-tree
id="tree-option"
ref="selectTree"
:accordion="accordion"
:data="options"
:props="objMap"
:node-key="objMap.value"
:expand-on-click-node="false"
:default-expanded-keys="defaultExpandedKey"
:filter-node-method="filterNode"
@node-click="handleNodeClick"
></el-tree>
</el-option>
</el-select>
</div>
</template>
<script setup>
const { proxy } = getCurrentInstance();
const props = defineProps({
/* 配置项 */
objMap: {
type: Object,
default: () => {
return {
value: 'id', // ID
label: 'label', //
children: 'children' //
}
}
},
/* 自动收起 */
accordion: {
type: Boolean,
default: () => {
return false
}
},
/**当前双向数据绑定的值 */
value: {
type: [String, Number],
default: ''
},
/**当前的数据 */
options: {
type: Array,
default: () => []
},
/**输入框内部的文字 */
placeholder: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:value']);
const valueId = computed({
get: () => props.value,
set: (val) => {
emit('update:value', val)
}
});
const valueTitle = ref('');
const defaultExpandedKey = ref([]);
function initHandle() {
nextTick(() => {
const selectedValue = valueId.value;
if(selectedValue && selectedValue !== null && typeof (selectedValue) !== "undefined"){
const node = proxy.$refs.selectTree.getNode(selectedValue)
if (node) {
valueTitle.value = node.data[props.objMap.label]
proxy.$refs.selectTree.setCurrentKey(selectedValue) //
defaultExpandedKey.value = [selectedValue] //
}
} else {
clearHandle()
}
})
}
function handleNodeClick(node) {
valueTitle.value = node[props.objMap.label]
valueId.value = node[props.objMap.value];
defaultExpandedKey.value = [];
proxy.$refs.treeSelect.blur()
selectFilterData('')
}
function selectFilterData(val) {
proxy.$refs.selectTree.filter(val)
}
function filterNode(value, data) {
if (!value) return true
return data[props.objMap['label']].indexOf(value) !== -1
}
function clearHandle() {
valueTitle.value = ''
valueId.value = ''
defaultExpandedKey.value = [];
clearSelected()
}
function clearSelected() {
const allNode = document.querySelectorAll('#tree-option .el-tree-node')
allNode.forEach((element) => element.classList.remove('is-current'))
}
onMounted(() => {
initHandle()
})
watch(valueId, () => {
initHandle();
})
</script>
<style lang='scss' scoped>
@import "@/assets/styles/variables.module.scss";
.el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
padding: 0;
background-color: #fff;
height: auto;
}
.el-select-dropdown__item.selected {
font-weight: normal;
}
ul li .el-tree .el-tree-node__content {
height: auto;
padding: 0 20px;
box-sizing: border-box;
}
:deep(.el-tree-node__content:hover),
:deep(.el-tree-node__content:active),
:deep(.is-current > div:first-child),
:deep(.el-tree-node__content:focus) {
background-color: mix(#fff, $--color-primary, 90%);
color: $--color-primary;
}
</style>

View File

@ -4,33 +4,28 @@
:src="url" :src="url"
frameborder="no" frameborder="no"
style="width: 100%; height: 100%" style="width: 100%; height: 100%"
scrolling="auto" scrolling="auto" />
/>
</div> </div>
</template> </template>
<script>
export default { <script setup>
props: { const props = defineProps({
src: { src: {
type: String, type: String,
default:"/" required: true
}
},
data() {
return {
height: document.documentElement.clientHeight - 94.5 + "px;",
loading: true,
url:this.src
};
},
mounted: function () {
setTimeout(() => {
this.loading = false;
}, 300);
const that = this;
window.onresize = function temp() {
that.height = document.documentElement.clientHeight - 94.5 + "px;";
};
} }
}; })
const height = ref(document.documentElement.clientHeight - 94.5 + "px;")
const loading = ref(true)
const url = computed(() => props.src)
onMounted(() => {
setTimeout(() => {
loading.value = false;
}, 300);
window.onresize = function temp() {
height.value = document.documentElement.clientHeight - 94.5 + "px;";
};
})
</script> </script>

View File

@ -1,23 +1,30 @@
// import hasRole from './permission/hasRole'
// import hasPermi from './permission/hasPermi'
// import dialogDrag from './dialog/drag'
// import dialogDragWidth from './dialog/dragWidth'
// import dialogDragHeight from './dialog/dragHeight'
// import clipboard from './module/clipboard'
//
// const install = function(Vue) {
// Vue.directive('hasRole', hasRole)
// Vue.directive('hasPermi', hasPermi)
// Vue.directive('clipboard', clipboard)
// Vue.directive('dialogDrag', dialogDrag)
// Vue.directive('dialogDragWidth', dialogDragWidth)
// Vue.directive('dialogDragHeight', dialogDragHeight)
// }
//
// if (window.Vue) {
// window['hasRole'] = hasRole
// window['hasPermi'] = hasPermi
// Vue.use(install); // eslint-disable-line
// }
//
// export default install
import hasRole from './permission/hasRole' import hasRole from './permission/hasRole'
import hasPermi from './permission/hasPermi' import hasPermi from './permission/hasPermi'
import dialogDrag from './dialog/drag'
import dialogDragWidth from './dialog/dragWidth'
import dialogDragHeight from './dialog/dragHeight'
import clipboard from './module/clipboard'
const install = function(Vue) { export default function directive(app){
Vue.directive('hasRole', hasRole) app.directive('hasRole', hasRole)
Vue.directive('hasPermi', hasPermi) app.directive('hasPermi', hasPermi)
Vue.directive('clipboard', clipboard)
Vue.directive('dialogDrag', dialogDrag)
Vue.directive('dialogDragWidth', dialogDragWidth)
Vue.directive('dialogDragHeight', dialogDragHeight)
} }
if (window.Vue) {
window['hasRole'] = hasRole
window['hasPermi'] = hasPermi
Vue.use(install); // eslint-disable-line
}
export default install

View File

@ -1,29 +1,22 @@
<template> <template>
<section class="app-main"> <section class="app-main">
<transition name="fade-transform" mode="out-in"> <router-view v-slot="{ Component, route }">
<keep-alive :include="cachedViews"> <transition name="fade-transform" mode="out-in">
<router-view v-if="!$route.meta.link" :key="key" /> <keep-alive :include="cachedViews">
</keep-alive> <component :is="Component" :key="route.path"/>
</transition> </keep-alive>
<iframe-toggle /> </transition>
</router-view>
</section> </section>
</template> </template>
<script> <script setup>
import iframeToggle from "./IframeToggle/index" let store = useStore()
const route = useRoute()
export default { store.dispatch('tagsView/addCachedView', route)
name: 'AppMain', const cachedViews = computed(() => {
components: { iframeToggle }, return store.state.tagsView.cachedViews
computed: { })
cachedViews() {
return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.path
}
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -55,21 +48,7 @@ export default {
// fix css style bug in open el-dialog // fix css style bug in open el-dialog
.el-popup-parent--hidden { .el-popup-parent--hidden {
.fixed-header { .fixed-header {
padding-right: 6px; padding-right: 17px;
} }
} }
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background-color: #c0c0c0;
border-radius: 3px;
}
</style> </style>

View File

@ -1,24 +1,20 @@
<template> <template>
<transition-group name="fade-transform" mode="out-in"> <transition-group name="fade-transform" mode="out-in">
<inner-link <inner-link
v-for="(item, index) in iframeViews" v-for="(item, index) in tagsViewStore.iframeViews"
:key="item.path" :key="item.path"
:iframeId="'iframe' + index" :iframeId="'iframe' + index"
v-show="$route.path === item.path" v-show="route.path === item.path"
:src="item.meta.link" :src="item.meta.link"
></inner-link> ></inner-link>
</transition-group> </transition-group>
</template> </template>
<script> <script setup>
import InnerLink from "../InnerLink/index" import InnerLink from "../InnerLink/index.vue";
import useTagsViewStore from '@/store/modules/tagsView';
const route = useRoute();
const tagsViewStore = useTagsViewStore()
export default {
components: { InnerLink },
computed: {
iframeViews() {
return this.$store.state.tagsView.iframeViews
}
}
}
</script> </script>

View File

@ -1,47 +1,30 @@
<template>
<div :style="'height:' + height" v-loading="loading" element-loading-text="正在加载页面,请稍候!">
<iframe
:id="iframeId"
style="width: 100%; height: 100%"
:src="src"
frameborder="no"
></iframe>
</div>
</template>
<script> <script>
export default { export default {
props: { setup() {
src: { const route = useRoute();
type: String, const link = route.meta.link;
default: "/" if (link === "") {
}, return "404";
iframeId: {
type: String
} }
let url = link;
const height = document.documentElement.clientHeight - 94.5 + "px";
const style = { height: height };
//
return () =>
h(
"div",
{
style: style,
},
h("iframe", {
src: url,
frameborder: "no",
width: "100%",
height: "100%",
scrolling: "auto",
})
);
}, },
data() {
return {
loading: false,
height: document.documentElement.clientHeight - 94.5 + "px;"
};
},
mounted() {
var _this = this;
const iframeId = ("#" + this.iframeId).replace(/\//g, "\\/");
const iframe = document.querySelector(iframeId);
// iframeloading
if (iframe.attachEvent) {
this.loading = true;
iframe.attachEvent("onload", function () {
_this.loading = false;
});
} else {
this.loading = true;
iframe.onload = function () {
_this.loading = false;
};
}
}
}; };
</script> </script>

View File

@ -1,14 +1,13 @@
<template> <template>
<div class="navbar"> <div class="navbar">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> <hamburger id="hamburger-container" :is-active="getters.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!$store.state.settings.topNav" />
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/> <top-nav id="topmenu-container" class="topmenu-container" v-if="$store.state.settings.topNav" />
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>
<div class="right-menu"> <div class="right-menu">
<template v-if="device!=='mobile'"> <template v-if="getters.device !== 'mobile'">
<search id="header-search" class="right-menu-item" /> <header-search id="header-search" class="right-menu-item" />
<el-tooltip content="源码地址" effect="dark" placement="bottom"> <el-tooltip content="源码地址" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" /> <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip>
@ -22,112 +21,99 @@
<el-tooltip content="布局大小" effect="dark" placement="bottom"> <el-tooltip content="布局大小" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" /> <size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip>
</template> </template>
<div class="avatar-container">
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click"> <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper"> <div class="avatar-wrapper">
<img :src="avatar" class="user-avatar"> <img :src="getters.avatar" class="user-avatar" />
<i class="el-icon-caret-bottom" /> <el-icon><caret-bottom /></el-icon>
</div> </div>
<el-dropdown-menu slot="dropdown"> <template #dropdown>
<router-link to="/user/profile"> <el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item> <router-link to="/user/profile">
</router-link> <el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item @click.native="setting = true"> </router-link>
<span>布局设置</span> <el-dropdown-item command="setLayout">
</el-dropdown-item> <span>布局设置</span>
<el-dropdown-item divided @click.native="logout"> </el-dropdown-item>
<span>退出登录</span> <el-dropdown-item divided command="logout">
</el-dropdown-item> <span>退出登录</span>
</el-dropdown-menu> </el-dropdown-item>
</el-dropdown> </el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup>
import { mapGetters } from 'vuex' import { ElMessageBox } from 'element-plus'
import Breadcrumb from '@/components/Breadcrumb' import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav' import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger' import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull' import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect' import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch' import HeaderSearch from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git' import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc' import RuoYiDoc from '@/components/RuoYi/Doc'
export default { const store = useStore();
components: { const getters = computed(() => store.getters);
Breadcrumb,
TopNav, function toggleSideBar() {
Hamburger, store.dispatch('app/toggleSideBar')
Screenfull, }
SizeSelect,
Search, function handleCommand(command) {
RuoYiGit, switch (command) {
RuoYiDoc case "setLayout":
}, setLayout();
computed: { break;
...mapGetters([ case "logout":
'sidebar', logout();
'avatar', break;
'device' default:
]), break;
setting: {
get() {
return this.$store.state.settings.showSettings
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'showSettings',
value: val
})
}
},
topNav: {
get() {
return this.$store.state.settings.topNav
}
}
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
async logout() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$store.dispatch('LogOut').then(() => {
location.href = '/index';
})
}).catch(() => {});
}
} }
} }
function logout() {
ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('LogOut').then(() => {
location.href = '/index';
})
}).catch(() => { });
}
const emits = defineEmits(['setLayout'])
function setLayout() {
emits('setLayout');
}
</script> </script>
<style lang="scss" scoped> <style lang='scss' scoped>
.navbar { .navbar {
height: 50px; height: 50px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background: #fff; background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08); box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container { .hamburger-container {
line-height: 46px; line-height: 46px;
height: 100%; height: 100%;
float: left; float: left;
cursor: pointer; cursor: pointer;
transition: background .3s; transition: background 0.3s;
-webkit-tap-highlight-color:transparent; -webkit-tap-highlight-color: transparent;
&:hover { &:hover {
background: rgba(0, 0, 0, .025) background: rgba(0, 0, 0, 0.025);
} }
} }
@ -149,6 +135,7 @@ export default {
float: right; float: right;
height: 100%; height: 100%;
line-height: 50px; line-height: 50px;
display: flex;
&:focus { &:focus {
outline: none; outline: none;
@ -164,16 +151,16 @@ export default {
&.hover-effect { &.hover-effect {
cursor: pointer; cursor: pointer;
transition: background .3s; transition: background 0.3s;
&:hover { &:hover {
background: rgba(0, 0, 0, .025) background: rgba(0, 0, 0, 0.025);
} }
} }
} }
.avatar-container { .avatar-container {
margin-right: 30px; margin-right: 40px;
.avatar-wrapper { .avatar-wrapper {
margin-top: 5px; margin-top: 5px;
@ -186,7 +173,7 @@ export default {
border-radius: 10px; border-radius: 10px;
} }
.el-icon-caret-bottom { i {
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
right: -20px; right: -20px;

View File

@ -1,260 +1,254 @@
<template> <template>
<el-drawer size="280px" :visible="visible" :with-header="false" :append-to-body="true" :show-close="false"> <el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px">
<div class="drawer-container"> <div class="setting-drawer-title">
<div> <h3 class="drawer-title">主题风格设置</h3>
<div class="setting-drawer-content"> </div>
<div class="setting-drawer-title"> <div class="setting-drawer-block-checbox">
<h3 class="drawer-title">主题风格设置</h3> <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
</div> <img src="@/assets/images/dark.svg" alt="dark" />
<div class="setting-drawer-block-checbox"> <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')"> <i aria-label="图标: check" class="anticon anticon-check">
<img src="@/assets/images/dark.svg" alt="dark"> <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;"> <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
<i aria-label="图标: check" class="anticon anticon-check"> </svg>
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class=""> </i>
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
</svg>
</i>
</div>
</div>
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
<img src="@/assets/images/light.svg" alt="light">
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
<i aria-label="图标: check" class="anticon anticon-check">
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class="">
<path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
</svg>
</i>
</div>
</div>
</div>
<div class="drawer-item">
<span>主题颜色</span>
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
</div>
</div> </div>
</div>
<el-divider/> <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
<img src="@/assets/images/light.svg" alt="light" />
<h3 class="drawer-title">系统布局配置</h3> <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
<i aria-label="图标: check" class="anticon anticon-check">
<div class="drawer-item"> <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
<span>开启 TopNav</span> <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
<el-switch v-model="topNav" class="drawer-switch" /> </svg>
</i>
</div> </div>
<div class="drawer-item">
<span>开启 Tags-Views</span>
<el-switch v-model="tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>固定 Header</span>
<el-switch v-model="fixedHeader" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>显示 Logo</span>
<el-switch v-model="sidebarLogo" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>动态标题</span>
<el-switch v-model="dynamicTitle" class="drawer-switch" />
</div>
<el-divider/>
<el-button size="small" type="primary" plain icon="el-icon-document-add" @click="saveSetting">保存配置</el-button>
<el-button size="small" plain icon="el-icon-refresh" @click="resetSetting">重置配置</el-button>
</div> </div>
</div> </div>
<div class="drawer-item">
<span>主题颜色</span>
<span class="comp-style">
<el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange"/>
</span>
</div>
<el-divider />
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>开启 TopNav</span>
<span class="comp-style">
<el-switch v-model="topNav" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>开启 Tags-Views</span>
<span class="comp-style">
<el-switch v-model="tagsView" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>固定 Header</span>
<span class="comp-style">
<el-switch v-model="fixedHeader" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>显示 Logo</span>
<span class="comp-style">
<el-switch v-model="sidebarLogo" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>动态标题</span>
<span class="comp-style">
<el-switch v-model="dynamicTitle" class="drawer-switch" />
</span>
</div>
<el-divider />
<el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
<el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
</el-drawer> </el-drawer>
</template> </template>
<script> <script setup>
import ThemePicker from '@/components/ThemePicker' import variables from '@/assets/styles/variables.module.scss'
import originElementPlus from 'element-plus/theme-chalk/index.css'
import axios from 'axios'
import { ElLoading, ElMessage } from 'element-plus'
import { useDynamicTitle } from '@/utils/dynamicTitle'
export default { const { proxy } = getCurrentInstance();
components: { ThemePicker }, const store = useStore();
data() { const showSettings = ref(false);
return { const theme = ref(store.state.settings.theme);
theme: this.$store.state.settings.theme, const sideTheme = ref(store.state.settings.sideTheme);
sideTheme: this.$store.state.settings.sideTheme const storeSettings = computed(() => store.state.settings);
}; const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]);
},
computed: { /** 是否需要topnav */
visible: { const topNav = computed({
get() { get: () => storeSettings.value.topNav,
return this.$store.state.settings.showSettings set: (val) => {
} store.dispatch('settings/changeSetting', {
}, key: 'topNav',
fixedHeader: { value: val
get() { })
return this.$store.state.settings.fixedHeader if (!val) {
}, store.commit("SET_SIDEBAR_ROUTERS", store.state.permission.defaultRoutes);
set(val) { }
this.$store.dispatch('settings/changeSetting', { }
key: 'fixedHeader', })
value: val /** 是否需要tagview */
}) const tagsView = computed({
} get: () => storeSettings.value.tagsView,
}, set: (val) => {
topNav: { store.dispatch('settings/changeSetting', {
get() { key: 'tagsView',
return this.$store.state.settings.topNav value: val
}, })
set(val) { }
this.$store.dispatch('settings/changeSetting', { })
key: 'topNav', /**是否需要固定头部 */
value: val const fixedHeader = computed({
}) get: () => storeSettings.value.fixedHeader,
if (!val) { set: (val) => {
this.$store.dispatch('app/toggleSideBarHide', false); store.dispatch('settings/changeSetting', {
this.$store.commit("SET_SIDEBAR_ROUTERS", this.$store.state.permission.defaultRoutes); key: 'fixedHeader',
} value: val
} })
}, }
tagsView: { })
get() { /**是否需要侧边栏的logo */
return this.$store.state.settings.tagsView const sidebarLogo = computed({
}, get: () => storeSettings.value.sidebarLogo,
set(val) { set: (val) => {
this.$store.dispatch('settings/changeSetting', { store.dispatch('settings/changeSetting', {
key: 'tagsView', key: 'sidebarLogo',
value: val value: val
}) })
} }
}, })
sidebarLogo: { /**是否需要侧边栏的动态网页的title */
get() { const dynamicTitle = computed({
return this.$store.state.settings.sidebarLogo get: () => storeSettings.value.dynamicTitle,
}, set: (val) => {
set(val) { store.dispatch('settings/changeSetting', {
this.$store.dispatch('settings/changeSetting', { key: 'dynamicTitle',
key: 'sidebarLogo', value: val
value: val })
}) //
} useDynamicTitle()
}, }
dynamicTitle: { })
get() {
return this.$store.state.settings.dynamicTitle function themeChange(val) {
}, store.dispatch('settings/changeSetting', {
set(val) { key: 'theme',
this.$store.dispatch('settings/changeSetting', { value: val
key: 'dynamicTitle', })
value: val theme.value = val;
}) }
} function handleTheme(val) {
}, store.dispatch('settings/changeSetting', {
}, key: 'sideTheme',
methods: { value: val
themeChange(val) { })
this.$store.dispatch('settings/changeSetting', { sideTheme.value = val;
key: 'theme', }
value: val function saveSetting() {
}) proxy.$modal.loading("正在保存到本地,请稍候...");
this.theme = val; let layoutSetting = {
}, "topNav": storeSettings.value.topNav,
handleTheme(val) { "tagsView": storeSettings.value.tagsView,
this.$store.dispatch('settings/changeSetting', { "fixedHeader": storeSettings.value.fixedHeader,
key: 'sideTheme', "sidebarLogo": storeSettings.value.sidebarLogo,
value: val "dynamicTitle": storeSettings.value.dynamicTitle,
}) "sideTheme": storeSettings.value.sideTheme,
this.sideTheme = val; "theme": storeSettings.value.theme
}, };
saveSetting() { localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
this.$modal.loading("正在保存到本地,请稍候..."); setTimeout(proxy.$modal.closeLoading(), 1000)
this.$cache.local.set( }
"layout-setting", function resetSetting() {
`{ proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
"topNav":${this.topNav}, localStorage.removeItem("layout-setting")
"tagsView":${this.tagsView}, setTimeout("window.location.reload()", 1000)
"fixedHeader":${this.fixedHeader}, }
"sidebarLogo":${this.sidebarLogo}, function openSetting() {
"dynamicTitle":${this.dynamicTitle}, showSettings.value = true;
"sideTheme":"${this.sideTheme}", }
"theme":"${this.theme}"
}` defineExpose({
); openSetting,
setTimeout(this.$modal.closeLoading(), 1000) })
}, </script>
resetSetting() {
this.$modal.loading("正在清除设置缓存并刷新,请稍候..."); <style lang='scss' scoped>
this.$cache.local.remove("layout-setting") .setting-drawer-title {
setTimeout("window.location.reload()", 1000) margin-bottom: 12px;
color: rgba(0, 0, 0, 0.85);
line-height: 22px;
font-weight: bold;
.drawer-title {
font-size: 14px;
}
}
.setting-drawer-block-checbox {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
.setting-drawer-block-checbox-item {
position: relative;
margin-right: 16px;
border-radius: 2px;
cursor: pointer;
img {
width: 48px;
height: 48px;
}
.custom-img {
width: 48px;
height: 38px;
border-radius: 5px;
box-shadow: 1px 1px 2px #898484;
}
.setting-drawer-block-checbox-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #1890ff;
font-weight: 700;
font-size: 14px;
} }
} }
} }
</script>
<style lang="scss" scoped> .drawer-item {
.setting-drawer-content { color: rgba(0, 0, 0, 0.65);
.setting-drawer-title { padding: 12px 0;
margin-bottom: 12px; font-size: 14px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
line-height: 22px;
font-weight: bold;
}
.setting-drawer-block-checbox { .comp-style {
display: flex; float: right;
justify-content: flex-start; margin: -3px 8px 0px 0px;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
.setting-drawer-block-checbox-item {
position: relative;
margin-right: 16px;
border-radius: 2px;
cursor: pointer;
img {
width: 48px;
height: 48px;
}
.setting-drawer-block-checbox-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #1890ff;
font-weight: 700;
font-size: 14px;
}
}
}
}
.drawer-container {
padding: 20px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
.drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
line-height: 22px;
}
.drawer-item {
color: rgba(0, 0, 0, .65);
font-size: 14px;
padding: 12px 0;
}
.drawer-switch {
float: right
}
} }
}
</style> </style>

View File

@ -1,33 +0,0 @@
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const { icon, title } = context.props
const vnodes = []
if (icon) {
vnodes.push(<svg-icon icon-class={icon}/>)
}
if (title) {
if (title.length > 5) {
vnodes.push(<span slot='title' title={(title)}>{(title)}</span>)
} else {
vnodes.push(<span slot='title'>{(title)}</span>)
}
}
return vnodes
}
}
</script>

View File

@ -1,43 +1,40 @@
<template> <template>
<component :is="type" v-bind="linkProps(to)"> <component :is="type" v-bind="linkProps()">
<slot /> <slot />
</component> </component>
</template> </template>
<script> <script setup>
import { isExternal } from '@/utils/validate' import { isExternal } from '@/utils/validate'
export default { const props = defineProps({
props: { to: {
to: { type: [String, Object],
type: [String, Object], required: true
required: true }
} })
},
computed: { const isExt = computed(() => {
isExternal() { return isExternal(props.to)
return isExternal(this.to) })
},
type() { const type = computed(() => {
if (this.isExternal) { if (isExt.value) {
return 'a' return 'a'
} }
return 'router-link' return 'router-link'
} })
},
methods: { function linkProps() {
linkProps(to) { if (isExt.value) {
if (this.isExternal) { return {
return { href: props.to,
href: to, target: '_blank',
target: '_blank', rel: 'noopener'
rel: 'noopener'
}
}
return {
to: to
}
} }
} }
return {
to: props.to
}
} }
</script> </script>

View File

@ -1,45 +1,32 @@
<template> <template>
<div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"> <div class="sidebar-logo-container" :class="{ 'collapse': collapse }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<transition name="sidebarLogoFade"> <transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" /> <img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1> <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
</router-link> </router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/"> <router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" /> <img v-if="logo" :src="logo" class="sidebar-logo" />
<h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1> <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1>
</router-link> </router-link>
</transition> </transition>
</div> </div>
</template> </template>
<script> <script setup>
import logoImg from '@/assets/logo/logo.png' import variables from '@/assets/styles/variables.module.scss'
import variables from '@/assets/styles/variables.scss' import logo from '@/assets/logo/logo.png'
export default { defineProps({
name: 'SidebarLogo', collapse: {
props: { type: Boolean,
collapse: { required: true
type: Boolean,
required: true
}
},
computed: {
variables() {
return variables;
},
sideTheme() {
return this.$store.state.settings.sideTheme
}
},
data() {
return {
title: process.env.VUE_APP_TITLE,
logo: logoImg
}
} }
} })
const title = ref('Ruoyi-Flex管理系统');
const store = useStore();
const sideTheme = computed(() => store.state.settings.sideTheme);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,17 +1,20 @@
<template> <template>
<div v-if="!item.hidden"> <div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"> <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)"> <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" /> <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
<template #title>{{ onlyOneChild.meta.title }}</template>
</el-menu-item> </el-menu-item>
</app-link> </app-link>
</template> </template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title"> <template v-if="item.meta" #title>
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> <svg-icon :icon-class="item.meta && item.meta.icon" />
<span>{{ item.meta.title }}</span>
</template> </template>
<sidebar-item <sidebar-item
v-for="child in item.children" v-for="child in item.children"
:key="child.path" :key="child.path"
@ -20,81 +23,72 @@
:base-path="resolvePath(child.path)" :base-path="resolvePath(child.path)"
class="nest-menu" class="nest-menu"
/> />
</el-submenu> </el-sub-menu>
</div> </div>
</template> </template>
<script> <script setup>
import path from 'path'
import { isExternal } from '@/utils/validate' import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link' import AppLink from './Link'
import FixiOSBug from './FixiOSBug' import { getNormalPath } from '@/utils/ruoyi'
export default { const props = defineProps({
name: 'SidebarItem', // route object
components: { Item, AppLink }, item: {
mixins: [FixiOSBug], type: Object,
props: { required: true
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
}, },
data() { isNest: {
this.onlyOneChild = null type: Boolean,
return {} default: false
}, },
methods: { basePath: {
hasOneShowingChild(children = [], parent) { type: String,
if (!children) { default: ''
children = [];
}
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath, routeQuery) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
if (routeQuery) {
let query = JSON.parse(routeQuery);
return { path: path.resolve(this.basePath, routePath), query: query }
}
return path.resolve(this.basePath, routePath)
}
} }
})
const onlyOneChild = ref({});
function hasOneShowingChild(children = [], parent) {
if (!children) {
children = [];
}
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
};
function resolvePath(routePath, routeQuery) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(props.basePath)) {
return props.basePath
}
if (routeQuery) {
let query = JSON.parse(routeQuery);
return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
}
return getNormalPath(props.basePath + '/' + routePath)
} }
</script> </script>

View File

@ -1,57 +1,49 @@
<template> <template>
<div :class="{'has-logo':showLogo}" :style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"> <div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<logo v-if="showLogo" :collapse="isCollapse" /> <logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper"> <el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
<el-menu <el-menu
:default-active="activeMenu" :default-active="activeMenu"
:collapse="isCollapse" :collapse="isCollapse"
:background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground" :background-color="sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
:text-color="settings.sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor" :text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
:unique-opened="true" :unique-opened="true"
:active-text-color="settings.theme" :active-text-color="theme"
:collapse-transition="false" :collapse-transition="false"
mode="vertical" mode="vertical"
> >
<sidebar-item <sidebar-item
v-for="(route, index) in sidebarRouters" v-for="(route, index) in sidebarRouters"
:key="route.path + index" :key="route.path + index"
:item="route" :item="route"
:base-path="route.path" :base-path="route.path"
/> />
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
</div> </div>
</template> </template>
<script> <script setup>
import { mapGetters, mapState } from "vuex"; import Logo from './Logo'
import Logo from "./Logo"; import SidebarItem from './SidebarItem'
import SidebarItem from "./SidebarItem"; import variables from '@/assets/styles/variables.module.scss'
import variables from "@/assets/styles/variables.scss";
const route = useRoute();
const store = useStore();
const sidebarRouters = computed(() => store.getters.sidebarRouters);
const showLogo = computed(() => store.state.settings.sidebarLogo);
const sideTheme = computed(() => store.state.settings.sideTheme);
const theme = computed(() => store.state.settings.theme);
const isCollapse = computed(() => !store.state.app.sidebar.opened);
const activeMenu = computed(() => {
const { meta, path } = route;
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu;
}
return path;
})
export default {
components: { SidebarItem, Logo },
computed: {
...mapState(["settings"]),
...mapGetters(["sidebarRouters", "sidebar"]),
activeMenu() {
const route = this.$route;
const { meta, path } = route;
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu;
}
return path;
},
showLogo() {
return this.$store.state.settings.sidebarLogo;
},
variables() {
return variables;
},
isCollapse() {
return !this.sidebar.opened;
}
}
};
</script> </script>

View File

@ -1,94 +1,112 @@
<template> <template>
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll"> <el-scrollbar
ref="scrollContainer"
:vertical="false"
class="scroll-container"
@wheel.prevent="handleScroll"
>
<slot /> <slot />
</el-scrollbar> </el-scrollbar>
</template> </template>
<script> <script setup>
const tagAndTagSpacing = 4 // tagAndTagSpacing import {
ref,
getCurrentInstance,
computed,
onMounted,
onBeforeUnmount,
} from 'vue';
import {useStore} from "vuex";
export default { const tagAndTagSpacing = ref(4);
name: 'ScrollPane', const { proxy } = getCurrentInstance();
data() {
return {
left: 0
}
},
computed: {
scrollWrapper() {
return this.$refs.scrollContainer.$refs.wrap
}
},
mounted() {
this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
},
beforeDestroy() {
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
},
methods: {
handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = this.scrollWrapper
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
},
emitScroll() {
this.$emit('scroll')
},
moveToTarget(currentTag) {
const $container = this.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = this.scrollWrapper
const tagList = this.$parent.$refs.tag
let firstTag = null const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef);
let lastTag = null
// find first tag and last tag onMounted(() => {
if (tagList.length > 0) { scrollWrapper.value.addEventListener('scroll', emitScroll, true)
firstTag = tagList[0] })
lastTag = tagList[tagList.length - 1] onBeforeUnmount(() => {
} scrollWrapper.value.removeEventListener('scroll', emitScroll)
})
if (firstTag === currentTag) { function handleScroll(e) {
$scrollWrapper.scrollLeft = 0 const eventDelta = e.wheelDelta || -e.deltaY * 40
} else if (lastTag === currentTag) { const $scrollWrapper = scrollWrapper.value;
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
} else { }
// find preTag and nextTag const emits = defineEmits()
const currentIndex = tagList.findIndex(item => item === currentTag) const emitScroll = () => {
const prevTag = tagList[currentIndex - 1] emits('scroll')
const nextTag = tagList[currentIndex + 1] }
// the tag's offsetLeft after of nextTag const store = useStore();
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing const visitedViews = computed(() => store.state.tagsView.visitedViews);
// the tag's offsetLeft before of prevTag function moveToTarget(currentTag) {
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing const $container = proxy.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = scrollWrapper.value;
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) { let firstTag = null
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth let lastTag = null
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft // find first tag and last tag
if (visitedViews.value.length > 0) {
firstTag = visitedViews.value[0]
lastTag = visitedViews.value[visitedViews.value.length - 1]
}
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
const tagListDom = document.getElementsByClassName('tags-view-item');
const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
let prevTag = null
let nextTag = null
for (const k in tagListDom) {
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
prevTag = tagListDom[k];
}
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
nextTag = tagListDom[k];
} }
} }
} }
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
}
} }
} }
defineExpose({
moveToTarget,
})
</script> </script>
<style lang="scss" scoped> <style lang='scss' scoped>
.scroll-container { .scroll-container {
white-space: nowrap; white-space: nowrap;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
::v-deep { :deep(.el-scrollbar__bar) {
.el-scrollbar__bar { bottom: 0px;
bottom: 0px; }
} :deep(.el-scrollbar__wrap) {
.el-scrollbar__wrap { height: 49px;
height: 49px;
}
} }
} }
</style> </style>

View File

@ -1,249 +1,245 @@
<template> <template>
<div id="tags-view-container" class="tags-view-container"> <div id="tags-view-container" class="tags-view-container">
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll"> <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
<router-link <router-link
v-for="tag in visitedViews" v-for="tag in visitedViews"
ref="tag"
:key="tag.path" :key="tag.path"
:class="isActive(tag)?'active':''" :data-path="tag.path"
:class="isActive(tag) ? 'active' : ''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item" class="tags-view-item"
:style="activeStyle(tag)" :style="activeStyle(tag)"
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''" @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent.native="openMenu(tag,$event)" @contextmenu.prevent="openMenu(tag, $event)"
> >
{{ tag.title }} {{ tag.title }}
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" /> <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
<close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
</span>
</router-link> </router-link>
</scroll-pane> </scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu"> <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)"><i class="el-icon-refresh-right"></i> 刷新页面</li> <li @click="refreshSelectedTag(selectedTag)">
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><i class="el-icon-close"></i> 关闭当前</li> <refresh-right style="width: 1em; height: 1em;" /> 刷新页面
<li @click="closeOthersTags"><i class="el-icon-circle-close"></i> 关闭其他</li> </li>
<li v-if="!isFirstView()" @click="closeLeftTags"><i class="el-icon-back"></i> 关闭左侧</li> <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<li v-if="!isLastView()" @click="closeRightTags"><i class="el-icon-right"></i> 关闭右侧</li> <close style="width: 1em; height: 1em;" /> 关闭当前
<li @click="closeAllTags(selectedTag)"><i class="el-icon-circle-close"></i> 全部关闭</li> </li>
<li @click="closeOthersTags">
<circle-close style="width: 1em; height: 1em;" /> 关闭其他
</li>
<li v-if="!isFirstView()" @click="closeLeftTags">
<back style="width: 1em; height: 1em;" /> 关闭左侧
</li>
<li v-if="!isLastView()" @click="closeRightTags">
<right style="width: 1em; height: 1em;" /> 关闭右侧
</li>
<li @click="closeAllTags(selectedTag)">
<circle-close style="width: 1em; height: 1em;" /> 全部关闭
</li>
</ul> </ul>
</div> </div>
</template> </template>
<script> <script setup>
import ScrollPane from './ScrollPane' import ScrollPane from './ScrollPane'
import path from 'path' import { getNormalPath } from '@/utils/ruoyi'
export default { const visible = ref(false);
components: { ScrollPane }, const top = ref(0);
data() { const left = ref(0);
return { const selectedTag = ref({});
visible: false, const affixTags = ref([]);
top: 0, const scrollPaneRef = ref(null);
left: 0,
selectedTag: {}, const { proxy } = getCurrentInstance();
affixTags: [] const store = useStore();
const route = useRoute();
const router = useRouter();
const visitedViews = computed(() => store.state.tagsView.visitedViews);
const routes = computed(() => store.state.permission.routes);
const theme = computed(() => store.state.settings.theme);
watch(route, () => {
addTags()
moveToCurrentTag()
})
watch(visible, (value) => {
if (value) {
document.body.addEventListener('click', closeMenu)
} else {
document.body.removeEventListener('click', closeMenu)
}
})
onMounted(() => {
initTags()
addTags()
})
function isActive(r) {
return r.path === route.path
}
function activeStyle(tag) {
if (!isActive(tag)) return {};
return {
"background-color": theme.value,
"border-color": theme.value
};
}
function isAffix(tag) {
return tag.meta && tag.meta.affix
}
function isFirstView() {
try {
return selectedTag.value.fullPath === visitedViews.value[1].fullPath || selectedTag.value.fullPath === '/index'
} catch (err) {
return false
}
}
function isLastView() {
try {
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
} catch (err) {
return false
}
}
function filterAffixTags(routes, basePath = '') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = getNormalPath(basePath + '/' + route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
})
} }
}, if (route.children) {
computed: { const tempTags = filterAffixTags(route.children, route.path)
visitedViews() { if (tempTags.length >= 1) {
return this.$store.state.tagsView.visitedViews tags = [...tags, ...tempTags]
},
routes() {
return this.$store.state.permission.routes
},
theme() {
return this.$store.state.settings.theme;
}
},
watch: {
$route() {
this.addTags()
this.moveToCurrentTag()
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
} }
} }
}, })
mounted() { return tags
this.initTags() }
this.addTags() function initTags() {
}, const res = filterAffixTags(routes.value);
methods: { affixTags.value = res;
isActive(route) { for (const tag of res) {
return route.path === this.$route.path // Must have tag name
}, if (tag.name) {
activeStyle(tag) { store.dispatch('tagsView/addVisitedView', tag)
if (!this.isActive(tag)) return {};
return {
"background-color": this.theme,
"border-color": this.theme
};
},
isAffix(tag) {
return tag.meta && tag.meta.affix
},
isFirstView() {
try {
return this.selectedTag.fullPath === '/index' || this.selectedTag.fullPath === this.visitedViews[1].fullPath
} catch (err) {
return false
}
},
isLastView() {
try {
return this.selectedTag.fullPath === this.visitedViews[this.visitedViews.length - 1].fullPath
} catch (err) {
return false
}
},
filterAffixTags(routes, basePath = '/') {
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
},
initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
for (const tag of affixTags) {
// Must have tag name
if (tag.name) {
this.$store.dispatch('tagsView/addVisitedView', tag)
}
}
},
addTags() {
const { name } = this.$route
if (name) {
this.$store.dispatch('tagsView/addView', this.$route)
if (this.$route.meta.link) {
this.$store.dispatch('tagsView/addIframeView', this.$route)
}
}
return false
},
moveToCurrentTag() {
const tags = this.$refs.tag
this.$nextTick(() => {
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
}
break
}
}
})
},
refreshSelectedTag(view) {
this.$tab.refreshPage(view);
if (this.$route.meta.link) {
this.$store.dispatch('tagsView/delIframeView', this.$route)
}
},
closeSelectedTag(view) {
this.$tab.closePage(view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view)
}
})
},
closeRightTags() {
this.$tab.closeRightPage(this.selectedTag).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews)
}
})
},
closeLeftTags() {
this.$tab.closeLeftPage(this.selectedTag).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
this.toLastView(visitedViews)
}
})
},
closeOthersTags() {
this.$router.push(this.selectedTag.fullPath).catch(()=>{});
this.$tab.closeOtherPage(this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$tab.closeAllPage().then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === this.$route.path)) {
return
}
this.toLastView(visitedViews, view)
})
},
toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
this.$router.replace({ path: '/redirect' + view.fullPath })
} else {
this.$router.push('/')
}
}
},
openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = e.clientX - offsetLeft + 15 // 15: margin right
if (left > maxLeft) {
this.left = maxLeft
} else {
this.left = left
}
this.top = e.clientY
this.visible = true
this.selectedTag = tag
},
closeMenu() {
this.visible = false
},
handleScroll() {
this.closeMenu()
} }
} }
} }
function addTags() {
const { name } = route
if (name) {
store.dispatch('tagsView/addView', route)
}
return false
}
function moveToCurrentTag() {
nextTick(() => {
for (const r of visitedViews.value) {
if (r.path === route.path) {
scrollPaneRef.value.moveToTarget(r);
// when query is different then update
if (r.fullPath !== route.fullPath) {
store.dispatch('tagsView/updateVisitedView', route)
}
}
}
})
}
function refreshSelectedTag(view) {
proxy.$tab.refreshPage(view);
}
function closeSelectedTag(view) {
proxy.$tab.closePage(view).then(({ visitedViews }) => {
if (isActive(view)) {
toLastView(visitedViews, view)
}
})
}
function closeRightTags() {
proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
toLastView(visitedViews)
}
})
}
function closeLeftTags() {
proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
toLastView(visitedViews)
}
})
}
function closeOthersTags() {
router.push(selectedTag.value).catch(() => { });
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
moveToCurrentTag()
})
}
function closeAllTags(view) {
proxy.$tab.closeAllPage().then(({ visitedViews }) => {
if (affixTags.value.some(tag => tag.path === route.path)) {
return
}
toLastView(visitedViews, view)
})
}
function toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
router.replace({ path: '/redirect' + view.fullPath })
} else {
router.push('/')
}
}
}
function openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
const offsetWidth = proxy.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const l = e.clientX - offsetLeft + 15 // 15: margin right
if (l > maxLeft) {
left.value = maxLeft
} else {
left.value = l
}
top.value = e.clientY
visible.value = true
selectedTag.value = tag
}
function closeMenu() {
visible.value = false
}
function handleScroll() {
closeMenu()
}
</script> </script>
<style lang="scss" scoped> <style lang='scss' scoped>
.tags-view-container { .tags-view-container {
height: 34px; height: 34px;
width: 100%; width: 100%;
background: #fff; background: #fff;
border-bottom: 1px solid #d8dce5; border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04); box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
.tags-view-wrapper { .tags-view-wrapper {
.tags-view-item { .tags-view-item {
display: inline-block; display: inline-block;
@ -269,7 +265,7 @@ export default {
color: #fff; color: #fff;
border-color: #42b983; border-color: #42b983;
&::before { &::before {
content: ''; content: "";
background: #fff; background: #fff;
display: inline-block; display: inline-block;
width: 8px; width: 8px;
@ -292,7 +288,7 @@ export default {
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
color: #333; color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li { li {
margin: 0; margin: 0;
padding: 7px 16px; padding: 7px 16px;
@ -315,16 +311,18 @@ export default {
vertical-align: 2px; vertical-align: 2px;
border-radius: 50%; border-radius: 50%;
text-align: center; text-align: center;
transition: all .3s cubic-bezier(.645, .045, .355, 1); transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: 100% 50%; transform-origin: 100% 50%;
&:before { &:before {
transform: scale(.6); transform: scale(0.6);
display: inline-block; display: inline-block;
vertical-align: -3px; vertical-align: -3px;
} }
&:hover { &:hover {
background-color: #b4bccc; background-color: #b4bccc;
color: #fff; color: #fff;
width: 12px !important;
height: 12px !important;
} }
} }
} }

View File

@ -1,5 +1,4 @@
export { default as AppMain } from './AppMain' export { default as AppMain } from './AppMain'
export { default as Navbar } from './Navbar' export { default as Navbar } from './Navbar'
export { default as Settings } from './Settings' export { default as Settings } from './Settings'
export { default as Sidebar } from './Sidebar/index.vue'
export { default as TagsView } from './TagsView/index.vue' export { default as TagsView } from './TagsView/index.vue'

View File

@ -1,111 +1,105 @@
<template> <template>
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}"> <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/> <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar v-if="!sidebar.hide" class="sidebar-container"/> <sidebar class="sidebar-container" />
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container"> <div :class="{ hasTagsView: needTagsView }" class="main-container">
<div :class="{'fixed-header':fixedHeader}"> <div :class="{ 'fixed-header': fixedHeader }">
<navbar/> <navbar @setLayout="setLayout" />
<tags-view v-if="needTagsView"/> <tags-view v-if="needTagsView" />
</div> </div>
<app-main/> <app-main />
<right-panel> <settings ref="settingRef" />
<settings/>
</right-panel>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup>
import RightPanel from '@/components/RightPanel' import { useWindowSize } from '@vueuse/core'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components' import Sidebar from './components/Sidebar/index.vue'
import ResizeMixin from './mixin/ResizeHandler' import { AppMain, Navbar, Settings, TagsView } from './components'
import { mapState } from 'vuex' import defaultSettings from '@/settings'
import variables from '@/assets/styles/variables.scss'
export default { const store = useStore();
name: 'Layout', const theme = computed(() => store.state.settings.theme);
components: { const sideTheme = computed(() => store.state.settings.sideTheme);
AppMain, const sidebar = computed(() => store.state.app.sidebar);
Navbar, const device = computed(() => store.state.app.device);
RightPanel, const needTagsView = computed(() => store.state.settings.tagsView);
Settings, const fixedHeader = computed(() => store.state.settings.fixedHeader);
Sidebar,
TagsView const classObj = computed(() => ({
}, hideSidebar: !sidebar.value.opened,
mixins: [ResizeMixin], openSidebar: sidebar.value.opened,
computed: { withoutAnimation: sidebar.value.withoutAnimation,
...mapState({ mobile: device.value === 'mobile'
theme: state => state.settings.theme, }))
sideTheme: state => state.settings.sideTheme,
sidebar: state => state.app.sidebar, const { width, height } = useWindowSize();
device: state => state.app.device, const WIDTH = 992; // refer to Bootstrap's responsive design
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader watchEffect(() => {
}), if (device.value === 'mobile' && sidebar.value.opened) {
classObj() { store.dispatch('app/closeSideBar', { withoutAnimation: false })
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
},
variables() {
return variables;
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
} }
if (width.value - 1 < WIDTH) {
store.dispatch('app/toggleDevice', 'mobile')
store.dispatch('app/closeSideBar', { withoutAnimation: true })
} else {
store.dispatch('app/toggleDevice', 'desktop')
}
})
function handleClickOutside() {
store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
const settingRef = ref(null);
function setLayout() {
settingRef.value.openSetting();
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~@/assets/styles/mixin.scss"; @import "@/assets/styles/mixin.scss";
@import "~@/assets/styles/variables.scss"; @import "@/assets/styles/variables.module.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar { .app-wrapper {
position: fixed; @include clearfix;
top: 0; position: relative;
} height: 100%;
} width: 100%;
.drawer-bg { &.mobile.openSidebar {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
} }
}
.hideSidebar .fixed-header { .drawer-bg {
width: calc(100% - 54px); background: #000;
} opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.sidebarHide .fixed-header { .fixed-header {
width: 100%; position: fixed;
} top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
}
.mobile .fixed-header { .hideSidebar .fixed-header {
width: 100%; width: calc(100% - 54px);
} }
.mobile .fixed-header {
width: 100%;
}
</style> </style>

View File

@ -1,45 +0,0 @@
import store from '@/store'
const { body } = document
const WIDTH = 992 // refer to Bootstrap's responsive design
export default {
watch: {
$route(route) {
if (this.device === 'mobile' && this.sidebar.opened) {
store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
},
beforeMount() {
window.addEventListener('resize', this.$_resizeHandler)
},
beforeDestroy() {
window.removeEventListener('resize', this.$_resizeHandler)
},
mounted() {
const isMobile = this.$_isMobile()
if (isMobile) {
store.dispatch('app/toggleDevice', 'mobile')
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_isMobile() {
const rect = body.getBoundingClientRect()
return rect.width - 1 < WIDTH
},
$_resizeHandler() {
if (!document.hidden) {
const isMobile = this.$_isMobile()
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
if (isMobile) {
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
}
}
}
}

View File

@ -1,86 +1,86 @@
import Vue from 'vue' import { createApp } from 'vue'
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import Element from 'element-ui' import ElementPlus from 'element-plus'
import './assets/styles/element-variables.scss' import locale from 'element-plus/lib/locale/lang/zh-cn' // 中文语言
import '@/assets/styles/index.scss' // global css import '@/assets/styles/index.scss' // global css
import '@/assets/styles/ruoyi.scss' // ruoyi css // element css
import 'element-plus/es/components/message/style/css';
import 'element-plus/es/components/message-box/style/css';
import 'element-plus/es/components/notification/style/css';
import 'element-plus/es/components/loading/style/css';
import App from './App' import App from './App'
import store from './store' import store from './store'
import router from './router' import router from './router'
import directive from './directive' // directive import directive from './directive' // directive
// 注册指令
import plugins from './plugins' // plugins import plugins from './plugins' // plugins
import { download } from '@/utils/request' import { download } from '@/utils/request'
import './assets/icons' // icon // svg图标
import 'virtual:svg-icons-register'; // 引入svg icon注册脚本
import SvgIcon from '@/components/SvgIcon'
import elementIcons from '@/components/SvgIcon/svgicon'
import './permission' // permission control import './permission' // permission control
import { getDicts } from "@/api/system/dict/data";
import { getConfigKey } from "@/api/system/config"; import { useDict } from '@/utils/dict'
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi"; import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel } from '@/utils/ruoyi'
// 分页组件 // 分页组件
import Pagination from "@/components/Pagination"; import Pagination from '@/components/Pagination'
// 自定义表格工具组件 // 自定义表格工具组件
import RightToolbar from "@/components/RightToolbar" import RightToolbar from '@/components/RightToolbar'
// 富文本组件
import Editor from "@/components/Editor"
// 文件上传组件 // 文件上传组件
import FileUpload from "@/components/FileUpload" import FileUpload from "@/components/FileUpload"
// 图片上传组件 // 图片上传组件
import ImageUpload from "@/components/ImageUpload" import ImageUpload from "@/components/ImageUpload"
// 图片预览组件 // 图片预览组件
import ImagePreview from "@/components/ImagePreview" import ImagePreview from "@/components/ImagePreview"
// 自定义树选择组件
import TreeSelect from '@/components/TreeSelect'
// 字典标签组件 // 字典标签组件
import DictTag from '@/components/DictTag' import DictTag from '@/components/DictTag'
// 头部标签组件
import VueMeta from 'vue-meta' const app = createApp(App)
// 字典数据组件
import DictData from '@/components/DictData'
// 全局方法挂载 // 全局方法挂载
Vue.prototype.getDicts = getDicts app.config.globalProperties.useDict = useDict
Vue.prototype.getConfigKey = getConfigKey app.config.globalProperties.download = download
Vue.prototype.parseTime = parseTime app.config.globalProperties.parseTime = parseTime
Vue.prototype.resetForm = resetForm app.config.globalProperties.resetForm = resetForm
Vue.prototype.addDateRange = addDateRange app.config.globalProperties.handleTree = handleTree
Vue.prototype.selectDictLabel = selectDictLabel app.config.globalProperties.addDateRange = addDateRange
Vue.prototype.selectDictLabels = selectDictLabels app.config.globalProperties.selectDictLabel = selectDictLabel
Vue.prototype.download = download
Vue.prototype.handleTree = handleTree
// 全局组件挂载 // 全局组件挂载
Vue.component('DictTag', DictTag) app.component('DictTag', DictTag)
Vue.component('Pagination', Pagination) app.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar) app.component('TreeSelect', TreeSelect)
Vue.component('Editor', Editor) app.component('FileUpload', FileUpload)
Vue.component('FileUpload', FileUpload) app.component('ImageUpload', ImageUpload)
Vue.component('ImageUpload', ImageUpload) app.component('ImagePreview', ImagePreview)
Vue.component('ImagePreview', ImagePreview) app.component('RightToolbar', RightToolbar)
Vue.use(directive) app.use(router)
Vue.use(plugins) app.use(store)
Vue.use(VueMeta) app.use(plugins)
DictData.install() app.use(elementIcons)
//app.component('svg-icon', SvgIcon)
app.component('SvgIcon', SvgIcon)
/** directive(app)
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online! ! !
*/
Vue.use(Element, { // 使用element-plus 并且设置全局的大小
size: Cookies.get('size') || 'medium' // set element-ui default size app.use(ElementPlus, {
locale: locale,
// 支持 large、default、small
size: Cookies.get('size') || 'default'
}) })
Vue.config.productionTip = false app.mount('#app')
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})

View File

@ -1,14 +1,14 @@
import router from './router' import router from './router'
import store from './store' import store from './store'
import { Message } from 'element-ui' import { ElMessage } from 'element-plus'
import NProgress from 'nprogress' import NProgress from 'nprogress'
import 'nprogress/nprogress.css' import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth' import { getToken } from '@/utils/auth'
import { isRelogin } from '@/utils/request' import { isHttp } from '@/utils/validate'
NProgress.configure({ showSpinner: false }) NProgress.configure({ showSpinner: false });
const whiteList = ['/login', '/register'] const whiteList = ['/login', '/auth-redirect', '/bind', '/register'];
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
NProgress.start() NProgress.start()
@ -20,21 +20,23 @@ router.beforeEach((to, from, next) => {
NProgress.done() NProgress.done()
} else { } else {
if (store.getters.roles.length === 0) { if (store.getters.roles.length === 0) {
isRelogin.show = true
// 判断当前用户是否已拉取完user_info信息 // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetInfo').then(() => { store.dispatch('GetInfo').then(() => {
isRelogin.show = false
store.dispatch('GenerateRoutes').then(accessRoutes => { store.dispatch('GenerateRoutes').then(accessRoutes => {
// 根据roles权限生成可访问的路由表 // 根据roles权限生成可访问的路由表
router.addRoutes(accessRoutes) // 动态添加可访问路由表 accessRoutes.forEach(route => {
if (!isHttp(route.path)) {
router.addRoute(route) // 动态添加可访问路由表
}
})
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
}) })
}).catch(err => { }).catch(err => {
store.dispatch('LogOut').then(() => { store.dispatch('LogOut').then(() => {
Message.error(err) ElMessage.error(err)
next({ path: '/' }) next({ path: '/' })
})
}) })
})
} else { } else {
next() next()
} }

View File

@ -1,42 +1,42 @@
import axios from 'axios' import axios from 'axios'
import { Message } from 'element-ui' import { ElMessage } from 'element-plus'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import { getToken } from '@/utils/auth' import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode' import errorCode from '@/utils/errorCode'
import { blobValidate } from "@/utils/ruoyi"; import { blobValidate } from '@/utils/ruoyi'
const baseURL = process.env.VUE_APP_BASE_API const baseURL = import.meta.env.VITE_APP_BASE_API
export default { export default {
name(name, isDelete = true) { name(name, isDelete = true) {
var url = baseURL + "/common/download?fileName=" + encodeURIComponent(name) + "&delete=" + isDelete var url = baseURL + "/common/download?fileName=" + encodeURI(name) + "&delete=" + isDelete
axios({ axios({
method: 'get', method: 'get',
url: url, url: url,
responseType: 'blob', responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() } headers: { 'Authorization': 'Bearer ' + getToken() }
}).then((res) => { }).then(async (res) => {
const isBlob = blobValidate(res.data); const isLogin = await blobValidate(res.data);
if (isBlob) { if (isLogin) {
const blob = new Blob([res.data]) const blob = new Blob([res.data])
this.saveAs(blob, decodeURIComponent(res.headers['download-filename'])) this.saveAs(blob, decodeURI(res.headers['download-filename']))
} else { } else {
this.printErrMsg(res.data); this.printErrMsg(res.data);
} }
}) })
}, },
resource(resource) { resource(resource) {
var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource); var url = baseURL + "/common/download/resource?resource=" + encodeURI(resource);
axios({ axios({
method: 'get', method: 'get',
url: url, url: url,
responseType: 'blob', responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() } headers: { 'Authorization': 'Bearer ' + getToken() }
}).then((res) => { }).then(async (res) => {
const isBlob = blobValidate(res.data); const isLogin = await blobValidate(res.data);
if (isBlob) { if (isLogin) {
const blob = new Blob([res.data]) const blob = new Blob([res.data])
this.saveAs(blob, decodeURIComponent(res.headers['download-filename'])) this.saveAs(blob, decodeURI(res.headers['download-filename']))
} else { } else {
this.printErrMsg(res.data); this.printErrMsg(res.data);
} }
@ -49,9 +49,9 @@ export default {
url: url, url: url,
responseType: 'blob', responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() } headers: { 'Authorization': 'Bearer ' + getToken() }
}).then((res) => { }).then(async (res) => {
const isBlob = blobValidate(res.data); const isLogin = await blobValidate(res.data);
if (isBlob) { if (isLogin) {
const blob = new Blob([res.data], { type: 'application/zip' }) const blob = new Blob([res.data], { type: 'application/zip' })
this.saveAs(blob, name) this.saveAs(blob, name)
} else { } else {
@ -66,7 +66,7 @@ export default {
const resText = await data.text(); const resText = await data.text();
const rspObj = JSON.parse(resText); const rspObj = JSON.parse(resText);
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
Message.error(errMsg); ElMessage.error(errMsg);
} }
} }

View File

@ -4,17 +4,15 @@ import cache from './cache'
import modal from './modal' import modal from './modal'
import download from './download' import download from './download'
export default { export default function installPlugins(app){
install(Vue) { // 页签操作
// 页签操作 app.config.globalProperties.$tab = tab
Vue.prototype.$tab = tab // 认证对象
// 认证对象 app.config.globalProperties.$auth = auth
Vue.prototype.$auth = auth // 缓存对象
// 缓存对象 app.config.globalProperties.$cache = cache
Vue.prototype.$cache = cache // 模态框对象
// 模态框对象 app.config.globalProperties.$modal = modal
Vue.prototype.$modal = modal // 下载文件
// 下载文件 app.config.globalProperties.$download = download
Vue.prototype.$download = download
}
} }

View File

@ -1,59 +1,59 @@
import { Message, MessageBox, Notification, Loading } from 'element-ui' import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
let loadingInstance; let loadingInstance;
export default { export default {
// 消息提示 // 消息提示
msg(content) { msg(content) {
Message.info(content) ElMessage.info(content)
}, },
// 错误消息 // 错误消息
msgError(content) { msgError(content) {
Message.error(content) ElMessage.error(content)
}, },
// 成功消息 // 成功消息
msgSuccess(content) { msgSuccess(content) {
Message.success(content) ElMessage.success(content)
}, },
// 警告消息 // 警告消息
msgWarning(content) { msgWarning(content) {
Message.warning(content) ElMessage.warning(content)
}, },
// 弹出提示 // 弹出提示
alert(content) { alert(content) {
MessageBox.alert(content, "系统提示") ElMessageBox.alert(content, "系统提示")
}, },
// 错误提示 // 错误提示
alertError(content) { alertError(content) {
MessageBox.alert(content, "系统提示", { type: 'error' }) ElMessageBox.alert(content, "系统提示", { type: 'error' })
}, },
// 成功提示 // 成功提示
alertSuccess(content) { alertSuccess(content) {
MessageBox.alert(content, "系统提示", { type: 'success' }) ElMessageBox.alert(content, "系统提示", { type: 'success' })
}, },
// 警告提示 // 警告提示
alertWarning(content) { alertWarning(content) {
MessageBox.alert(content, "系统提示", { type: 'warning' }) ElMessageBox.alert(content, "系统提示", { type: 'warning' })
}, },
// 通知提示 // 通知提示
notify(content) { notify(content) {
Notification.info(content) ElNotification.info(content)
}, },
// 错误通知 // 错误通知
notifyError(content) { notifyError(content) {
Notification.error(content); ElNotification.error(content);
}, },
// 成功通知 // 成功通知
notifySuccess(content) { notifySuccess(content) {
Notification.success(content) ElNotification.success(content)
}, },
// 警告通知 // 警告通知
notifyWarning(content) { notifyWarning(content) {
Notification.warning(content) ElNotification.warning(content)
}, },
// 确认窗体 // 确认窗体
confirm(content) { confirm(content) {
return MessageBox.confirm(content, "系统提示", { return ElMessageBox.confirm(content, "系统提示", {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: "warning", type: "warning",
@ -61,7 +61,7 @@ export default {
}, },
// 提交内容 // 提交内容
prompt(content) { prompt(content) {
return MessageBox.prompt(content, "系统提示", { return ElMessageBox.prompt(content, "系统提示", {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: "warning", type: "warning",
@ -69,10 +69,9 @@ export default {
}, },
// 打开遮罩层 // 打开遮罩层
loading(content) { loading(content) {
loadingInstance = Loading.service({ loadingInstance = ElLoading.service({
lock: true, lock: true,
text: content, text: content,
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)", background: "rgba(0, 0, 0, 0.7)",
}) })
}, },

View File

@ -1,10 +1,10 @@
import store from '@/store' import store from '@/store'
import router from '@/router'; import router from '@/router'
export default { export default {
// 刷新当前tab页签 // 刷新当前tab页签
refreshPage(obj) { refreshPage(obj) {
const { path, query, matched } = router.currentRoute; const { path, query, matched } = router.currentRoute.value;
if (obj === undefined) { if (obj === undefined) {
matched.forEach((m) => { matched.forEach((m) => {
if (m.components && m.components.default && m.components.default.name) { if (m.components && m.components.default && m.components.default.name) {
@ -24,7 +24,7 @@ export default {
}, },
// 关闭当前tab页签打开新页签 // 关闭当前tab页签打开新页签
closeOpenPage(obj) { closeOpenPage(obj) {
store.dispatch("tagsView/delView", router.currentRoute); store.dispatch("tagsView/delView", router.currentRoute.value);
if (obj !== undefined) { if (obj !== undefined) {
return router.push(obj); return router.push(obj);
} }
@ -32,12 +32,8 @@ export default {
// 关闭指定tab页签 // 关闭指定tab页签
closePage(obj) { closePage(obj) {
if (obj === undefined) { if (obj === undefined) {
return store.dispatch('tagsView/delView', router.currentRoute).then(({ visitedViews }) => { return store.dispatch('tagsView/delView', router.currentRoute.value).then(({ lastPath }) => {
const latestView = visitedViews.slice(-1)[0] return router.push(lastPath || '/index');
if (latestView) {
return router.push(latestView.fullPath)
}
return router.push('/');
}); });
} }
return store.dispatch('tagsView/delView', obj); return store.dispatch('tagsView/delView', obj);
@ -48,21 +44,19 @@ export default {
}, },
// 关闭左侧tab页签 // 关闭左侧tab页签
closeLeftPage(obj) { closeLeftPage(obj) {
return store.dispatch('tagsView/delLeftTags', obj || router.currentRoute); return store.dispatch('tagsView/delLeftTags', obj || router.currentRoute.value);
}, },
// 关闭右侧tab页签 // 关闭右侧tab页签
closeRightPage(obj) { closeRightPage(obj) {
return store.dispatch('tagsView/delRightTags', obj || router.currentRoute); return store.dispatch('tagsView/delRightTags', obj || router.currentRoute.value);
}, },
// 关闭其他tab页签 // 关闭其他tab页签
closeOtherPage(obj) { closeOtherPage(obj) {
return store.dispatch('tagsView/delOthersViews', obj || router.currentRoute); return store.dispatch('tagsView/delOthersViews', obj || router.currentRoute.value);
}, },
// 添加tab页签 // 打开tab页签
openPage(title, url, params) { openPage(url) {
var obj = { path: url, meta: { title: title } } return router.push(url);
store.dispatch('tagsView/addView', obj);
return router.push({ path: url, query: params });
}, },
// 修改tab页签 // 修改tab页签
updatePage(obj) { updatePage(obj) {

View File

@ -1,9 +1,4 @@
import Vue from 'vue' import { createWebHistory, createRouter } from 'vue-router'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout' import Layout from '@/layout'
/** /**
@ -17,8 +12,6 @@ import Layout from '@/layout'
* redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
* name:'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题 * name:'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
* query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数 * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
* roles: ['admin', 'common'] // 访问路由的角色权限
* permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
* meta : { * meta : {
noCache: true // 如果设置为true则不会被 <keep-alive> 缓存(默认 false) noCache: true // 如果设置为true则不会被 <keep-alive> 缓存(默认 false)
title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字 title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
@ -37,7 +30,7 @@ export const constantRoutes = [
children: [ children: [
{ {
path: '/redirect/:path(.*)', path: '/redirect/:path(.*)',
component: () => import('@/views/redirect') component: () => import('@/views/redirect/index.vue')
} }
] ]
}, },
@ -52,7 +45,7 @@ export const constantRoutes = [
hidden: true hidden: true
}, },
{ {
path: '/404', path: "/:pathMatch(.*)*",
component: () => import('@/views/error/404'), component: () => import('@/views/error/404'),
hidden: true hidden: true
}, },
@ -64,10 +57,10 @@ export const constantRoutes = [
{ {
path: '', path: '',
component: Layout, component: Layout,
redirect: 'index', redirect: '/index',
children: [ children: [
{ {
path: 'index', path: '/index',
component: () => import('@/views/index'), component: () => import('@/views/index'),
name: 'Index', name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true } meta: { title: '首页', icon: 'dashboard', affix: true }
@ -87,16 +80,11 @@ export const constantRoutes = [
meta: { title: '个人中心', icon: 'user' } meta: { title: '个人中心', icon: 'user' }
} }
] ]
} },
]
// 动态路由,基于用户权限动态去加载
export const dynamicRoutes = [
{ {
path: '/system/user-auth', path: '/system/user-auth',
component: Layout, component: Layout,
hidden: true, hidden: true,
permissions: ['system:user:edit'],
children: [ children: [
{ {
path: 'role/:userId(\\d+)', path: 'role/:userId(\\d+)',
@ -110,7 +98,6 @@ export const dynamicRoutes = [
path: '/system/role-auth', path: '/system/role-auth',
component: Layout, component: Layout,
hidden: true, hidden: true,
permissions: ['system:role:edit'],
children: [ children: [
{ {
path: 'user/:roleId(\\d+)', path: 'user/:roleId(\\d+)',
@ -124,7 +111,6 @@ export const dynamicRoutes = [
path: '/system/dict-data', path: '/system/dict-data',
component: Layout, component: Layout,
hidden: true, hidden: true,
permissions: ['system:dict:list'],
children: [ children: [
{ {
path: 'index/:dictId(\\d+)', path: 'index/:dictId(\\d+)',
@ -138,32 +124,91 @@ export const dynamicRoutes = [
path: '/tool/gen-edit', path: '/tool/gen-edit',
component: Layout, component: Layout,
hidden: true, hidden: true,
permissions: ['tool:gen:edit'],
children: [ children: [
{ {
path: 'index/:tableId(\\d+)', path: 'index',
component: () => import('@/views/tool/gen/editTable'), component: () => import('@/views/tool/gen/editTable'),
name: 'GenEdit', name: 'GenEdit',
meta: { title: '修改生成配置', activeMenu: '/tool/gen' } meta: { title: '修改生成配置', activeMenu: '/tool/gen' }
} }
] ]
} }
] ];
// 防止连续点击多次路由报错 // 动态路由,基于用户权限动态去加载
let routerPush = Router.prototype.push; export const dynamicRoutes= [
let routerReplace = Router.prototype.replace; {
// push path: '/system/user-auth',
Router.prototype.push = function push(location) { component: Layout,
return routerPush.call(this, location).catch(err => err) hidden: true,
} permissions: ['system:user:edit'],
// replace children: [
Router.prototype.replace = function push(location) { {
return routerReplace.call(this, location).catch(err => err) path: 'role/:userId(\\d+)',
} component: () => import('@/views/system/user/authRole.vue'),
name: 'AuthRole',
meta: { title: '分配角色', activeMenu: '/system/user', icon: '' }
}
]
},
{
path: '/system/role-auth',
component: Layout,
hidden: true,
permissions: ['system:role:edit'],
children: [
{
path: 'user/:roleId(\\d+)',
component: () => import('@/views/system/role/authUser.vue'),
name: 'AuthUser',
meta: { title: '分配用户', activeMenu: '/system/role', icon: '' }
}
]
},
{
path: '/system/dict-data',
component: Layout,
hidden: true,
permissions: ['system:dict:list'],
children: [
{
path: 'index/:dictId(\\d+)',
component: () => import('@/views/system/dict/data.vue'),
name: 'Data',
meta: { title: '字典数据', activeMenu: '/system/dict', icon: '' }
}
]
},
{
path: '/tool/gen-edit',
component: Layout,
hidden: true,
permissions: ['tool:gen:edit'],
children: [
{
path: 'index/:tableId(\\d+)',
component: () => import('@/views/tool/gen/editTable.vue'),
name: 'GenEdit',
meta: { title: '修改生成配置', activeMenu: '/tool/gen', icon: '' }
}
]
}
];
export default new Router({ /**
mode: 'history', // 去掉url中的# * 创建路由
scrollBehavior: () => ({ y: 0 }), */
routes: constantRoutes const router = createRouter({
}) history: createWebHistory(import.meta.env.VITE_APP_CONTEXT_PATH),
routes: constantRoutes,
// 刷新时,滚动条位置还原
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
},
});
export default router;

View File

@ -1,9 +1,12 @@
module.exports = { export default {
/**
* 网页标题
*/
title: import.meta.env.VITE_APP_TITLE,
/** /**
* 侧边栏主题 深色主题theme-dark浅色主题theme-light * 侧边栏主题 深色主题theme-dark浅色主题theme-light
*/ */
sideTheme: 'theme-dark', sideTheme: 'theme-dark',
/** /**
* 是否系统布局配置 * 是否系统布局配置
*/ */

View File

@ -2,7 +2,6 @@ const getters = {
sidebar: state => state.app.sidebar, sidebar: state => state.app.sidebar,
size: state => state.app.size, size: state => state.app.size,
device: state => state.app.device, device: state => state.app.device,
dict: state => state.dict.dict,
visitedViews: state => state.tagsView.visitedViews, visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews, cachedViews: state => state.tagsView.cachedViews,
token: state => state.user.token, token: state => state.user.token,

View File

@ -1,25 +1,21 @@
import Vue from 'vue' import { createStore } from 'vuex'
import Vuex from 'vuex'
import app from './modules/app' import app from './modules/app'
import dict from './modules/dict'
import user from './modules/user' import user from './modules/user'
import tagsView from './modules/tagsView' import tagsView from './modules/tagsView'
import permission from './modules/permission' import permission from './modules/permission'
import settings from './modules/settings' import settings from './modules/settings'
import getters from './getters' import getters from './getters'
Vue.use(Vuex) const store = createStore({
const store = new Vuex.Store({
modules: { modules: {
app, app,
dict,
user, user,
tagsView, tagsView,
permission, permission,
settings settings
}, },
getters getters
}) });
export default store export default store

View File

@ -1,10 +1,12 @@
import auth from '@/plugins/auth'
import router, { constantRoutes, dynamicRoutes } from '@/router' import router, { constantRoutes, dynamicRoutes } from '@/router'
import { getRouters } from '@/api/menu' import { getRouters } from '@/api/menu'
import Layout from '@/layout/index' import Layout from '@/layout/index'
import ParentView from '@/components/ParentView' import ParentView from '@/components/ParentView'
import InnerLink from '@/layout/components/InnerLink' import InnerLink from '@/layout/components/InnerLink'
// 匹配views里面所有的.vue文件
const modules = import.meta.glob('./../../views/**/*.vue')
const permission = { const permission = {
state: { state: {
routes: [], routes: [],
@ -36,15 +38,14 @@ const permission = {
getRouters().then(res => { getRouters().then(res => {
const sdata = JSON.parse(JSON.stringify(res.data)) const sdata = JSON.parse(JSON.stringify(res.data))
const rdata = JSON.parse(JSON.stringify(res.data)) const rdata = JSON.parse(JSON.stringify(res.data))
const defaultData = JSON.parse(JSON.stringify(res.data))
const sidebarRoutes = filterAsyncRouter(sdata) const sidebarRoutes = filterAsyncRouter(sdata)
const rewriteRoutes = filterAsyncRouter(rdata, false, true) const rewriteRoutes = filterAsyncRouter(rdata, false, true)
const asyncRoutes = filterDynamicRoutes(dynamicRoutes); const defaultRoutes = filterAsyncRouter(defaultData)
rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
router.addRoutes(asyncRoutes);
commit('SET_ROUTES', rewriteRoutes) commit('SET_ROUTES', rewriteRoutes)
commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes)) commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
commit('SET_DEFAULT_ROUTES', sidebarRoutes) commit('SET_DEFAULT_ROUTES', sidebarRoutes)
commit('SET_TOPBAR_ROUTES', sidebarRoutes) commit('SET_TOPBAR_ROUTES', defaultRoutes)
resolve(rewriteRoutes) resolve(rewriteRoutes)
}) })
}) })
@ -122,12 +123,14 @@ export function filterDynamicRoutes(routes) {
} }
export const loadView = (view) => { export const loadView = (view) => {
if (process.env.NODE_ENV === 'development') { let res;
return (resolve) => require([`@/views/${view}`], resolve) for (const path in modules) {
} else { const dir = path.split('views/')[1].split('.vue')[0];
// 使用 import 实现生产环境的路由懒加载 if (dir === view) {
return () => import(`@/views/${view}`) res = () => modules[path]();
}
} }
return res;
} }
export default permission export default permission

View File

@ -1,5 +1,6 @@
import { login, logout, getInfo } from '@/api/login' import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth' import { getToken, setToken, removeToken } from '@/utils/auth'
import defAva from '@/assets/images/profile.jpg'
const user = { const user = {
state: { state: {
@ -52,7 +53,7 @@ const user = {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getInfo().then(res => { getInfo().then(res => {
const user = res.user const user = res.user
const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar; const avatar = (user.avatar == "" || user.avatar == null) ? defAva : import.meta.env.VITE_APP_BASE_API + user.avatar;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组 if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
commit('SET_ROLES', res.roles) commit('SET_ROLES', res.roles)
commit('SET_PERMISSIONS', res.permissions) commit('SET_PERMISSIONS', res.permissions)

View File

@ -0,0 +1,17 @@
import { getDicts } from '@/api/system/dict/data'
/**
* 获取字典数据
*/
export function useDict(...args) {
const res = ref({});
return (() => {
args.forEach((d, index) => {
res.value[d] = [];
getDicts(d).then(resp => {
res.value[d] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass }))
})
})
return toRefs(res.value);
})()
}

View File

@ -1,82 +0,0 @@
import Vue from 'vue'
import { mergeRecursive } from "@/utils/ruoyi";
import DictMeta from './DictMeta'
import DictData from './DictData'
const DEFAULT_DICT_OPTIONS = {
types: [],
}
/**
* @classdesc 字典
* @property {Object} label 标签对象内部属性名为字典类型名称
* @property {Object} dict 字段数组内部属性名为字典类型名称
* @property {Array.<DictMeta>} _dictMetas 字典元数据数组
*/
export default class Dict {
constructor() {
this.owner = null
this.label = {}
this.type = {}
}
init(options) {
if (options instanceof Array) {
options = { types: options }
}
const opts = mergeRecursive(DEFAULT_DICT_OPTIONS, options)
if (opts.types === undefined) {
throw new Error('need dict types')
}
const ps = []
this._dictMetas = opts.types.map(t => DictMeta.parse(t))
this._dictMetas.forEach(dictMeta => {
const type = dictMeta.type
Vue.set(this.label, type, {})
Vue.set(this.type, type, [])
if (dictMeta.lazy) {
return
}
ps.push(loadDict(this, dictMeta))
})
return Promise.all(ps)
}
/**
* 重新加载字典
* @param {String} type 字典类型
*/
reloadDict(type) {
const dictMeta = this._dictMetas.find(e => e.type === type)
if (dictMeta === undefined) {
return Promise.reject(`the dict meta of ${type} was not found`)
}
return loadDict(this, dictMeta)
}
}
/**
* 加载字典
* @param {Dict} dict 字典
* @param {DictMeta} dictMeta 字典元数据
* @returns {Promise}
*/
function loadDict(dict, dictMeta) {
return dictMeta.request(dictMeta)
.then(response => {
const type = dictMeta.type
let dicts = dictMeta.responseConverter(response, dictMeta)
if (!(dicts instanceof Array)) {
console.error('the return of responseConverter must be Array.<DictData>')
dicts = []
} else if (dicts.filter(d => d instanceof DictData).length !== dicts.length) {
console.error('the type of elements in dicts must be DictData')
dicts = []
}
dict.type[type].splice(0, Number.MAX_SAFE_INTEGER, ...dicts)
dicts.forEach(d => {
Vue.set(dict.label[type], d.value, d.label)
})
return dicts
})
}

View File

@ -1,17 +0,0 @@
import DictOptions from './DictOptions'
import DictData from './DictData'
export default function(dict, dictMeta) {
const label = determineDictField(dict, dictMeta.labelField, ...DictOptions.DEFAULT_LABEL_FIELDS)
const value = determineDictField(dict, dictMeta.valueField, ...DictOptions.DEFAULT_VALUE_FIELDS)
return new DictData(dict[label], dict[value], dict)
}
/**
* 确定字典字段
* @param {DictData} dict
* @param {...String} fields
*/
function determineDictField(dict, ...fields) {
return fields.find(f => Object.prototype.hasOwnProperty.call(dict, f))
}

View File

@ -1,13 +0,0 @@
/**
* @classdesc 字典数据
* @property {String} label 标签
* @property {*} value 标签
* @property {Object} raw 原始数据
*/
export default class DictData {
constructor(label, value, raw) {
this.label = label
this.value = value
this.raw = raw
}
}

View File

@ -1,38 +0,0 @@
import { mergeRecursive } from "@/utils/ruoyi";
import DictOptions from './DictOptions'
/**
* @classdesc 字典元数据
* @property {String} type 类型
* @property {Function} request 请求
* @property {String} label 标签字段
* @property {String} value 值字段
*/
export default class DictMeta {
constructor(options) {
this.type = options.type
this.request = options.request
this.responseConverter = options.responseConverter
this.labelField = options.labelField
this.valueField = options.valueField
this.lazy = options.lazy === true
}
}
/**
* 解析字典元数据
* @param {Object} options
* @returns {DictMeta}
*/
DictMeta.parse= function(options) {
let opts = null
if (typeof options === 'string') {
opts = DictOptions.metas[options] || {}
opts.type = options
} else if (typeof options === 'object') {
opts = options
}
opts = mergeRecursive(DictOptions.metas['*'], opts)
return new DictMeta(opts)
}

View File

@ -1,51 +0,0 @@
import { mergeRecursive } from "@/utils/ruoyi";
import dictConverter from './DictConverter'
export const options = {
metas: {
'*': {
/**
* 字典请求方法签名为function(dictMeta: DictMeta): Promise
*/
request: (dictMeta) => {
console.log(`load dict ${dictMeta.type}`)
return Promise.resolve([])
},
/**
* 字典响应数据转换器方法签名为function(response: Object, dictMeta: DictMeta): DictData
*/
responseConverter,
labelField: 'label',
valueField: 'value',
},
},
/**
* 默认标签字段
*/
DEFAULT_LABEL_FIELDS: ['label', 'name', 'title'],
/**
* 默认值字段
*/
DEFAULT_VALUE_FIELDS: ['value', 'id', 'uid', 'key'],
}
/**
* 映射字典
* @param {Object} response 字典数据
* @param {DictMeta} dictMeta 字典元数据
* @returns {DictData}
*/
function responseConverter(response, dictMeta) {
const dicts = response.content instanceof Array ? response.content : response
if (dicts === undefined) {
console.warn(`no dict data of "${dictMeta.type}" found in the response`)
return []
}
return dicts.map(d => dictConverter(d, dictMeta))
}
export function mergeOptions(src) {
mergeRecursive(options, src)
}
export default options

View File

@ -1,33 +0,0 @@
import Dict from './Dict'
import { mergeOptions } from './DictOptions'
export default function(Vue, options) {
mergeOptions(options)
Vue.mixin({
data() {
if (this.$options === undefined || this.$options.dicts === undefined || this.$options.dicts === null) {
return {}
}
const dict = new Dict()
dict.owner = this
return {
dict
}
},
created() {
if (!(this.dict instanceof Dict)) {
return
}
options.onCreated && options.onCreated(this.dict)
this.dict.init(this.$options.dicts).then(() => {
options.onReady && options.onReady(this.dict)
this.$nextTick(() => {
this.$emit('dictReady', this.dict)
if (this.$options.methods && this.$options.methods.onDictReady instanceof Function) {
this.$options.methods.onDictReady.call(this, this.dict)
}
})
})
},
})
}

View File

@ -0,0 +1,13 @@
import store from '@/store'
import defaultSettings from '@/settings'
/**
* 动态修改标题
*/
export function useDynamicTitle() {
if (store.state.settings.dynamicTitle) {
document.title = store.state.settings.title + ' - ' + defaultSettings.title;
} else {
document.title = defaultSettings.title;
}
}

View File

@ -1,16 +1,16 @@
import { parseTime } from './ruoyi' import { parseTime } from '@/ruoyi'
/** /**
* 表格时间格式化 * 表格时间格式化
*/ */
export function formatDate(cellValue) { export function formatDate(cellValue) {
if (cellValue == null || cellValue == "") return ""; if (cellValue == null || cellValue == "") return "";
var date = new Date(cellValue) var date = new Date(cellValue)
var year = date.getFullYear() var year = date.getFullYear()
var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1 var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours() var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
} }
@ -330,7 +330,7 @@ export function makeMap(str, expectsLowerCase) {
? val => map[val.toLowerCase()] ? val => map[val.toLowerCase()]
: val => map[val] : val => map[val]
} }
export const exportDefault = 'export default ' export const exportDefault = 'export default '
export const beautifierConf = { export const beautifierConf = {
@ -387,4 +387,4 @@ export function camelCase(str) {
export function isNumberStr(str) { export function isNumberStr(str) {
return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str) return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
} }

View File

@ -1,21 +1,21 @@
import axios from 'axios' import axios from 'axios'
import { Notification, MessageBox, Message, Loading } from 'element-ui' import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
import store from '@/store' import store from '@/store'
import { getToken } from '@/utils/auth' import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode' import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from "@/utils/ruoyi"; import { tansParams, blobValidate } from '@/utils/ruoyi'
import cache from '@/plugins/cache' import cache from '@/plugins/cache'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
let downloadLoadingInstance; let downloadLoadingInstance;
// 是否显示重新登录 // 是否显示重新登录
export let isRelogin = { show: false }; let isReloginShow;
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8' axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
// axios中请求配置有baseURL选项表示请求URL公共部分 // axios中请求配置有baseURL选项表示请求URL公共部分
baseURL: process.env.VUE_APP_BASE_API, baseURL: import.meta.env.VITE_APP_BASE_API,
// 超时 // 超时
timeout: 10000 timeout: 10000
}) })
@ -46,10 +46,10 @@ service.interceptors.request.use(config => {
if (sessionObj === undefined || sessionObj === null || sessionObj === '') { if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj) cache.session.setJSON('sessionObj', requestObj)
} else { } else {
const s_url = sessionObj.url; // 请求地址 const s_url = sessionObj.url; // 请求地址
const s_data = sessionObj.data; // 请求数据 const s_data = sessionObj.data; // 请求数据
const s_time = sessionObj.time; // 请求时间 const s_time = sessionObj.time; // 请求时间
const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交 const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) { if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交'; const message = '数据正在处理,请勿重复提交';
console.warn(`[${s_url}]: ` + message) console.warn(`[${s_url}]: ` + message)
@ -61,8 +61,7 @@ service.interceptors.request.use(config => {
} }
return config return config
}, error => { }, error => {
console.log(error) Promise.reject(error)
Promise.reject(error)
}) })
// 响应拦截器 // 响应拦截器
@ -72,73 +71,84 @@ service.interceptors.response.use(res => {
// 获取错误信息 // 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default'] const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回 // 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') { if(res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer'){
return res.data return res.data
} }
if (code === 401) { if (code === 401) {
if (!isRelogin.show) { if (!isReloginShow) {
isRelogin.show = true; isReloginShow = true;
MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => { ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
isRelogin.show = false; confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
isReloginShow = false;
store.dispatch('LogOut').then(() => { store.dispatch('LogOut').then(() => {
location.href = '/index'; location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index';
}) })
}).catch(() => { }).catch(() => {
isRelogin.show = false; isReloginShow = false;
}); });
} }
return Promise.reject('无效的会话,或者会话已过期,请重新登录。') return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) { } else if (code === 500) {
Message({ message: msg, type: 'error' }) ElMessage({
return Promise.reject(new Error(msg)) message: msg,
} else if (code === 601) { type: 'error'
Message({ message: msg, type: 'warning' }) });
return Promise.reject('error') return Promise.reject(new Error(msg));
} else if (code !== 200) { } else if (code !== 200) {
Notification.error({ title: msg }) ElNotification.error({
title: msg
})
return Promise.reject('error') return Promise.reject('error')
} else { } else {
return res.data return Promise.resolve(res.data)
} }
}, },
error => { error => {
console.log('err' + error)
let { message } = error; let { message } = error;
if (message == "Network Error") { if (message == "Network Error") {
message = "后端接口连接异常"; message = "后端接口连接异常";
} else if (message.includes("timeout")) { }
else if (message.includes("timeout")) {
message = "系统接口请求超时"; message = "系统接口请求超时";
} else if (message.includes("Request failed with status code")) { }
else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常"; message = "系统接口" + message.substr(message.length - 3) + "异常";
} }
Message({ message: message, type: 'error', duration: 5 * 1000 }) ElMessage({
message: message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error) return Promise.reject(error)
} }
) )
// 通用下载方法 // 通用下载方法
export function download(url, params, filename, config) { export function download(url, params, filename) {
downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", }) downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
return service.post(url, params, { return service.post(url, params, {
transformRequest: [(params) => { return tansParams(params) }], transformRequest: [(params) => { return tansParams(params) }],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'blob', responseType: 'blob'
...config
}).then(async (data) => { }).then(async (data) => {
const isBlob = blobValidate(data); const isLogin = await blobValidate(data);
if (isBlob) { if (isLogin) {
const blob = new Blob([data]) const blob = new Blob([data])
saveAs(blob, filename) saveAs(blob, filename)
} else { } else {
const resText = await data.text(); const resText = await data.text();
const rspObj = JSON.parse(resText); const rspObj = JSON.parse(resText);
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default'] const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
Message.error(errMsg); ElMessage.error(errMsg);
} }
downloadLoadingInstance.close(); downloadLoadingInstance.close();
}).catch((r) => { }).catch((r) => {
console.error(r) console.error(r)
Message.error('下载文件出现错误,请联系管理员!') ElMessage.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close(); downloadLoadingInstance.close();
}) })
} }

View File

@ -227,7 +227,28 @@ export function tansParams(params) {
return result return result
} }
// 验证是否为blob格式 // 返回项目路径
export function blobValidate(data) { export function getNormalPath(p) {
return data.type !== 'application/json' if (p.length === 0 || !p || p == 'undefined') {
return p
};
let res = p.replace('//', '/')
if (res[res.length - 1] === '/') {
return res.slice(0, res.length - 1)
}
return res;
}
// 验证是否为blob格式
// export function blobValidate(data) {
// return data.type !== 'application/json'
// }
export async function blobValidate(data) {
try {
const text = await data.text();
JSON.parse(text);
return false;
} catch (error) {
return true;
}
} }

View File

@ -1,3 +1,12 @@
/**
* 判断url是否是http或https
* @param {string} path
* @returns {Boolean}
*/
export function isHttp(url) {
return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
}
/** /**
* @param {string} path * @param {string} path
* @returns {Boolean} * @returns {Boolean}

View File

@ -1,102 +0,0 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts';
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'
const animationDuration = 6000
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { //
type: 'shadow' // 线'line' | 'shadow'
}
},
grid: {
top: 10,
left: '2%',
right: '2%',
bottom: '3%',
containLabel: true
},
xAxis: [{
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisTick: {
alignWithLabel: true
}
}],
yAxis: [{
type: 'value',
axisTick: {
show: false
}
}],
series: [{
name: 'pageA',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [79, 52, 200, 334, 390, 330, 220],
animationDuration
}, {
name: 'pageB',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [80, 52, 200, 334, 390, 330, 220],
animationDuration
}, {
name: 'pageC',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [30, 52, 200, 334, 390, 330, 220],
animationDuration
}]
})
}
}
}
</script>

View File

@ -1,135 +0,0 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts';
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '350px'
},
autoResize: {
type: Boolean,
default: true
},
chartData: {
type: Object,
required: true
}
},
data() {
return {
chart: null
}
},
watch: {
chartData: {
deep: true,
handler(val) {
this.setOptions(val)
}
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.setOptions(this.chartData)
},
setOptions({ expectedData, actualData } = {}) {
this.chart.setOption({
xAxis: {
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
boundaryGap: false,
axisTick: {
show: false
}
},
grid: {
left: 10,
right: 10,
bottom: 20,
top: 30,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
legend: {
data: ['expected', 'actual']
},
series: [{
name: 'expected', itemStyle: {
normal: {
color: '#FF005A',
lineStyle: {
color: '#FF005A',
width: 2
}
}
},
smooth: true,
type: 'line',
data: expectedData,
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: 'actual',
smooth: true,
type: 'line',
itemStyle: {
normal: {
color: '#3888fa',
lineStyle: {
color: '#3888fa',
width: 2
},
areaStyle: {
color: '#f3f8ff'
}
}
},
data: actualData,
animationDuration: 2800,
animationEasing: 'quadraticOut'
}]
})
}
}
}
</script>

View File

@ -1,181 +0,0 @@
<template>
<el-row :gutter="40" class="panel-group">
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('newVisitis')">
<div class="card-panel-icon-wrapper icon-people">
<svg-icon icon-class="peoples" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
访客
</div>
<count-to :start-val="0" :end-val="102400" :duration="2600" class="card-panel-num" />
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('messages')">
<div class="card-panel-icon-wrapper icon-message">
<svg-icon icon-class="message" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
消息
</div>
<count-to :start-val="0" :end-val="81212" :duration="3000" class="card-panel-num" />
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('purchases')">
<div class="card-panel-icon-wrapper icon-money">
<svg-icon icon-class="money" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
金额
</div>
<count-to :start-val="0" :end-val="9280" :duration="3200" class="card-panel-num" />
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('shoppings')">
<div class="card-panel-icon-wrapper icon-shopping">
<svg-icon icon-class="shopping" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">
订单
</div>
<count-to :start-val="0" :end-val="13600" :duration="3600" class="card-panel-num" />
</div>
</div>
</el-col>
</el-row>
</template>
<script>
import CountTo from 'vue-count-to'
export default {
components: {
CountTo
},
methods: {
handleSetLineChartData(type) {
this.$emit('handleSetLineChartData', type)
}
}
}
</script>
<style lang="scss" scoped>
.panel-group {
margin-top: 18px;
.card-panel-col {
margin-bottom: 32px;
}
.card-panel {
height: 108px;
cursor: pointer;
font-size: 12px;
position: relative;
overflow: hidden;
color: #666;
background: #fff;
box-shadow: 4px 4px 40px rgba(0, 0, 0, .05);
border-color: rgba(0, 0, 0, .05);
&:hover {
.card-panel-icon-wrapper {
color: #fff;
}
.icon-people {
background: #40c9c6;
}
.icon-message {
background: #36a3f7;
}
.icon-money {
background: #f4516c;
}
.icon-shopping {
background: #34bfa3
}
}
.icon-people {
color: #40c9c6;
}
.icon-message {
color: #36a3f7;
}
.icon-money {
color: #f4516c;
}
.icon-shopping {
color: #34bfa3
}
.card-panel-icon-wrapper {
float: left;
margin: 14px 0 0 14px;
padding: 16px;
transition: all 0.38s ease-out;
border-radius: 6px;
}
.card-panel-icon {
float: left;
font-size: 48px;
}
.card-panel-description {
float: right;
font-weight: bold;
margin: 26px;
margin-left: 0px;
.card-panel-text {
line-height: 18px;
color: rgba(0, 0, 0, 0.45);
font-size: 16px;
margin-bottom: 12px;
}
.card-panel-num {
font-size: 20px;
}
}
}
}
@media (max-width:550px) {
.card-panel-description {
display: none;
}
.card-panel-icon-wrapper {
float: none !important;
width: 100%;
height: 100%;
margin: 0 !important;
.svg-icon {
display: block;
margin: 14px auto !important;
float: none !important;
}
}
}
</style>

View File

@ -1,79 +0,0 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts';
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
left: 'center',
bottom: '10',
data: ['Industries', 'Technology', 'Forex', 'Gold', 'Forecasts']
},
series: [
{
name: 'WEEKLY WRITE ARTICLES',
type: 'pie',
roseType: 'radius',
radius: [15, 95],
center: ['50%', '38%'],
data: [
{ value: 320, name: 'Industries' },
{ value: 240, name: 'Technology' },
{ value: 149, name: 'Forex' },
{ value: 100, name: 'Gold' },
{ value: 59, name: 'Forecasts' }
],
animationEasing: 'cubicInOut',
animationDuration: 2600
}
]
})
}
}
}
</script>

View File

@ -1,116 +0,0 @@
<template>
<div :class="className" :style="{height:height,width:width}" />
</template>
<script>
import * as echarts from 'echarts';
require('echarts/theme/macarons') // echarts theme
import resize from './mixins/resize'
const animationDuration = 3000
export default {
mixins: [resize],
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data() {
return {
chart: null
}
},
mounted() {
this.$nextTick(() => {
this.initChart()
})
},
beforeDestroy() {
if (!this.chart) {
return
}
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { //
type: 'shadow' // 线'line' | 'shadow'
}
},
radar: {
radius: '66%',
center: ['50%', '42%'],
splitNumber: 8,
splitArea: {
areaStyle: {
color: 'rgba(127,95,132,.3)',
opacity: 1,
shadowBlur: 45,
shadowColor: 'rgba(0,0,0,.5)',
shadowOffsetX: 0,
shadowOffsetY: 15
}
},
indicator: [
{ name: 'Sales', max: 10000 },
{ name: 'Administration', max: 20000 },
{ name: 'Information Techology', max: 20000 },
{ name: 'Customer Support', max: 20000 },
{ name: 'Development', max: 20000 },
{ name: 'Marketing', max: 20000 }
]
},
legend: {
left: 'center',
bottom: '10',
data: ['Allocated Budget', 'Expected Spending', 'Actual Spending']
},
series: [{
type: 'radar',
symbolSize: 0,
areaStyle: {
normal: {
shadowBlur: 13,
shadowColor: 'rgba(0,0,0,.2)',
shadowOffsetX: 0,
shadowOffsetY: 10,
opacity: 1
}
},
data: [
{
value: [5000, 7000, 12000, 11000, 15000, 14000],
name: 'Allocated Budget'
},
{
value: [4000, 9000, 15000, 15000, 13000, 11000],
name: 'Expected Spending'
},
{
value: [5500, 11000, 12000, 15000, 12000, 12000],
name: 'Actual Spending'
}
],
animationDuration: animationDuration
}]
})
}
}
}
</script>

View File

@ -1,56 +0,0 @@
import { debounce } from '@/utils'
export default {
data() {
return {
$_sidebarElm: null,
$_resizeHandler: null
}
},
mounted() {
this.initListener()
},
activated() {
if (!this.$_resizeHandler) {
// avoid duplication init
this.initListener()
}
// when keep-alive chart activated, auto resize
this.resize()
},
beforeDestroy() {
this.destroyListener()
},
deactivated() {
this.destroyListener()
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_sidebarResizeHandler(e) {
if (e.propertyName === 'width') {
this.$_resizeHandler()
}
},
initListener() {
this.$_resizeHandler = debounce(() => {
this.resize()
}, 100)
window.addEventListener('resize', this.$_resizeHandler)
this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
},
destroyListener() {
window.removeEventListener('resize', this.$_resizeHandler)
this.$_resizeHandler = null
this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
},
resize() {
const { chart } = this
chart && chart.resize()
}
}
}

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> <el-form :model="queryParams" ref="queryFormRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="客户姓名" prop="customerName"> <el-form-item label="客户姓名" prop="customerName">
<el-input <el-input
v-model="queryParams.customerName" v-model="queryParams.customerName"
@ -20,7 +20,7 @@
<el-form-item label="客户性别" prop="sex"> <el-form-item label="客户性别" prop="sex">
<el-select v-model="queryParams.sex" placeholder="请选择客户性别" clearable> <el-select v-model="queryParams.sex" placeholder="请选择客户性别" clearable>
<el-option <el-option
v-for="dict in dict.type.sys_user_sex" v-for="dict in sys_user_sex"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -31,13 +31,14 @@
<el-date-picker clearable <el-date-picker clearable
v-model="queryParams.birthday" v-model="queryParams.birthday"
type="date" type="date"
value-format="yyyy-MM-dd" format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择客户生日"> placeholder="请选择客户生日">
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -46,8 +47,7 @@
<el-button <el-button
type="primary" type="primary"
plain plain
icon="el-icon-plus" icon="Plus"
size="mini"
@click="handleAdd" @click="handleAdd"
v-hasPermi="['demo:customer:add']" v-hasPermi="['demo:customer:add']"
>新增</el-button> >新增</el-button>
@ -56,8 +56,7 @@
<el-button <el-button
type="success" type="success"
plain plain
icon="el-icon-edit" icon="Edit"
size="mini"
:disabled="single" :disabled="single"
@click="handleUpdate" @click="handleUpdate"
v-hasPermi="['demo:customer:edit']" v-hasPermi="['demo:customer:edit']"
@ -67,8 +66,7 @@
<el-button <el-button
type="danger" type="danger"
plain plain
icon="el-icon-delete" icon="Delete"
size="mini"
:disabled="multiple" :disabled="multiple"
@click="handleDelete" @click="handleDelete"
v-hasPermi="['demo:customer:remove']" v-hasPermi="['demo:customer:remove']"
@ -78,13 +76,12 @@
<el-button <el-button
type="warning" type="warning"
plain plain
icon="el-icon-download" icon="Download"
size="mini"
@click="handleExport" @click="handleExport"
v-hasPermi="['demo:customer:export']" v-hasPermi="['demo:customer:export']"
>导出</el-button> >导出</el-button>
</el-col> </el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row> </el-row>
<el-table v-loading="loading" :data="customerList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="customerList" @selection-change="handleSelectionChange">
@ -93,47 +90,45 @@
<el-table-column label="客户姓名" align="center" prop="customerName" /> <el-table-column label="客户姓名" align="center" prop="customerName" />
<el-table-column label="手机号码" align="center" prop="phonenumber" /> <el-table-column label="手机号码" align="center" prop="phonenumber" />
<el-table-column label="客户性别" align="center" prop="sex"> <el-table-column label="客户性别" align="center" prop="sex">
<template slot-scope="scope"> <template #default="scope">
<dict-tag :options="dict.type.sys_user_sex" :value="scope.row.sex"/> <dict-tag :options="sys_user_sex" :value="scope.row.sex"/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="客户生日" align="center" prop="birthday" width="180"> <el-table-column label="客户生日" align="center" prop="birthday" width="180">
<template slot-scope="scope"> <template #default="scope">
<span>{{ parseTime(scope.row.birthday, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.birthday, '{y}-{m}-{d}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="客户描述" align="center" prop="remark" /> <el-table-column label="客户描述" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope"> <template #default="scope">
<el-button <el-button
size="mini"
type="text" type="text"
icon="el-icon-edit" icon="Edit"
@click="handleUpdate(scope.row)" @click="handleUpdate(scope.row)"
v-hasPermi="['demo:customer:edit']" v-hasPermi="['demo:customer:edit']"
>修改</el-button> >修改</el-button>
<el-button <el-button
size="mini"
type="text" type="text"
icon="el-icon-delete" icon="Delete"
@click="handleDelete(scope.row)" @click="handleDelete(scope.row)"
v-hasPermi="['demo:customer:remove']" v-hasPermi="['demo:customer:remove']"
>删除</el-button> >删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<pagination <pagination
v-show="total>0" v-show="total>0"
:total="total" :total="total"
:page.sync="queryParams.pageNum" v-model:page="queryParams.pageNum"
:limit.sync="queryParams.pageSize" v-model:limit="queryParams.pageSize"
@pagination="getList" @pagination="getList"
/> />
<!-- 添加或修改客户主表(mb)对话框 --> <!-- 添加或修改客户主表(mb)对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body> <el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> <el-form ref="customerRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="客户姓名" prop="customerName"> <el-form-item label="客户姓名" prop="customerName">
<el-input v-model="form.customerName" placeholder="请输入客户姓名" /> <el-input v-model="form.customerName" placeholder="请输入客户姓名" />
</el-form-item> </el-form-item>
@ -143,7 +138,7 @@
<el-form-item label="客户性别" prop="sex"> <el-form-item label="客户性别" prop="sex">
<el-select v-model="form.sex" placeholder="请选择客户性别"> <el-select v-model="form.sex" placeholder="请选择客户性别">
<el-option <el-option
v-for="dict in dict.type.sys_user_sex" v-for="dict in sys_user_sex"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -154,7 +149,8 @@
<el-date-picker clearable <el-date-picker clearable
v-model="form.birthday" v-model="form.birthday"
type="date" type="date"
value-format="yyyy-MM-dd" format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择客户生日"> placeholder="请选择客户生日">
</el-date-picker> </el-date-picker>
</el-form-item> </el-form-item>
@ -164,40 +160,40 @@
<el-divider content-position="center">商品子信息</el-divider> <el-divider content-position="center">商品子信息</el-divider>
<el-row :gutter="10" class="mb8"> <el-row :gutter="10" class="mb8">
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAddDemoGoods">添加</el-button> <el-button type="primary" icon="Plus" @click="handleAddDemoGoods">添加</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="danger" icon="el-icon-delete" size="mini" @click="handleDeleteDemoGoods">删除</el-button> <el-button type="danger" icon="Delete" @click="handleDeleteDemoGoods">删除</el-button>
</el-col> </el-col>
</el-row> </el-row>
<el-table :data="demoGoodsList" :row-class-name="rowDemoGoodsIndex" @selection-change="handleDemoGoodsSelectionChange" ref="demoGoods"> <el-table :data="demoGoodsList" :row-class-name="rowDemoGoodsIndex" @selection-change="handleDemoGoodsSelectionChange" ref="demoGoods">
<el-table-column type="selection" width="50" align="center" /> <el-table-column type="selection" width="50" align="center" />
<el-table-column label="序号" align="center" prop="index" width="50"/> <el-table-column label="序号" align="center" prop="index" width="50"/>
<el-table-column label="商品名称" prop="name" width="150"> <el-table-column label="商品名称" prop="name" width="150">
<template slot-scope="scope"> <template #default="scope">
<el-input v-model="scope.row.name" placeholder="请输入商品名称" /> <el-input v-model="scope.row.name" placeholder="请输入商品名称" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="商品重量" prop="weight" width="150"> <el-table-column label="商品重量" prop="weight" width="150">
<template slot-scope="scope"> <template #default="scope">
<el-input v-model="scope.row.weight" placeholder="请输入商品重量" /> <el-input v-model="scope.row.weight" placeholder="请输入商品重量" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="商品价格" prop="price" width="150"> <el-table-column label="商品价格" prop="price" width="150">
<template slot-scope="scope"> <template #default="scope">
<el-input v-model="scope.row.price" placeholder="请输入商品价格" /> <el-input v-model="scope.row.price" placeholder="请输入商品价格" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="商品时间" prop="date" width="240"> <el-table-column label="商品时间" prop="date" width="240">
<template slot-scope="scope"> <template #default="scope">
<el-date-picker clearable v-model="scope.row.date" type="date" value-format="yyyy-MM-dd" placeholder="请选择商品时间" /> <el-date-picker clearable v-model="scope.row.date" type="date" value-format="YYYY-MM-DD" placeholder="请选择商品时间" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="商品种类" prop="type" width="150"> <el-table-column label="商品种类" prop="type" width="150">
<template slot-scope="scope"> <template #default="scope">
<el-select v-model="scope.row.type" placeholder="请选择商品种类"> <el-select v-model="scope.row.type" placeholder="请选择商品种类">
<el-option <el-option
v-for="dict in dict.type.sys_goods_type" v-for="dict in sys_goods_type"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -207,191 +203,196 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <template #footer>
<el-button type="primary" @click="submitForm"> </el-button> <div class="dialog-footer">
<el-button @click="cancel"> </el-button> <el-button type="primary" @click="submitForm"> </el-button>
</div> <el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script> <script setup>
import { listCustomer, getCustomer, delCustomer, addCustomer, updateCustomer } from "@/api/demo/customer"; import { listCustomer, getCustomer, delCustomer, addCustomer, updateCustomer } from "@/api/demo/customer";
const { proxy } = getCurrentInstance();
const {sys_goods_type, sys_user_sex } = proxy.useDict('sys_goods_type', 'sys_user_sex');
export default { //
name: "Customer", const loading = ref(true);
dicts: ['sys_goods_type', 'sys_user_sex'], //
data() { const ids = ref([]);
return { //
// const checkedDemoGoods= ref([])
loading: true, //
// const single = ref(true);
ids: [], //
// const multiple = ref(true);
checkedDemoGoods: [], //
// const total = ref(0);
single: true, //
// const open = ref(false);
multiple: true, //
// const title = ref("");
showSearch: true, //
// const showSearch = ref(true);
total: 0, // (mb)
// (mb) const customerList = ref([]);
customerList: [], //
// const demoGoodsList = ref([]);
demoGoodsList: [],
// const data = reactive({
title: "", //
// form: {},
open: false, //
// queryParams: {
queryParams: { pageNum: 1,
pageNum: 1, pageSize: 10,
pageSize: 10, customerName: undefined,
customerName: null, phonenumber: undefined,
phonenumber: null, sex: undefined,
sex: null, birthday: undefined
birthday: null,
},
//
form: {},
//
rules: {
}
};
}, },
created() { //
this.getList(); rules: {
}, customerName: [{ required: true, message: "客户姓名不能为空", trigger: "blur" }],
methods: { phonenumber: [{required: true, message: "手机号码不能为空", trigger: "blur" }],
/** 查询客户主表(mb)列表 */ sex: [{ required: true, message: "性别不能为空", trigger: "change" }],
getList() { birthday: [{ required: true, message: "生日不能为空", trigger: "blur" }]
this.loading = true; }
listCustomer(this.queryParams).then(response => { });
this.customerList = response.rows;
this.total = response.total; const { form, queryParams, rules } = toRefs(data);
this.loading = false;
}); /** 查询客户主表(mb)列表 */
}, function getList() {
// loading.value = true;
cancel() { listCustomer(queryParams.value).then(response => {
this.open = false; customerList.value = response.rows;
this.reset(); total.value = response.total;
}, loading.value = false;
// });
reset() { }
this.form = { //
customerId: null, function cancel() {
customerName: null, open.value = false;
phonenumber: null, reset();
sex: null, }
birthday: null, //
remark: null function reset() {
}; form.value = {
this.demoGoodsList = []; customerId: undefined,
this.resetForm("form"); customerName: undefined,
}, phonenumber: undefined,
/** 搜索按钮操作 */ sex: undefined,
handleQuery() { birthday: undefined,
this.queryParams.pageNum = 1; remark: undefined
this.getList(); };
}, demoGoodsList.value = [];
/** 重置按钮操作 */ proxy.resetForm("customerRef");
resetQuery() { }
this.resetForm("queryForm");
this.handleQuery(); /** 搜索按钮操作 */
}, function handleQuery() {
// queryParams.value.pageNum = 1;
handleSelectionChange(selection) { getList();
this.ids = selection.map(item => item.customerId) }
this.single = selection.length!==1 /** 重置按钮操作 */
this.multiple = !selection.length function resetQuery() {
}, proxy.resetForm("queryFormRef");
/** 新增按钮操作 */ handleQuery();
handleAdd() { }
this.reset(); //
this.open = true; function handleSelectionChange(selection) {
this.title = "添加客户主表(mb)"; ids.value = selection.map(item => item.customerId)
}, single.value = selection.length!==1
/** 修改按钮操作 */ multiple.value = !selection.length
handleUpdate(row) { }
this.reset(); /** 新增按钮操作 */
const customerId = row.customerId || this.ids function handleAdd() {
getCustomer(customerId).then(response => { reset();
this.form = response.data; open.value = true;
this.demoGoodsList = response.data.demoGoodsList; title.value = "添加客户主表(mb)";
this.open = true; }
this.title = "修改客户主表(mb)"; /** 修改按钮操作 */
}); function handleUpdate(row) {
}, reset();
/** 提交按钮 */ const customerId = row.customerId || ids.value
submitForm() { getCustomer(customerId).then(response => {
this.$refs["form"].validate(valid => { form.value = response.data;
if (valid) { demoGoodsList.value = response.data.demoGoodsList;
this.form.demoGoodsList = this.demoGoodsList; open.value = true;
if (this.form.customerId != null) { title.value = "修改客户主表(mb)";
updateCustomer(this.form).then(response => { });
this.$modal.msgSuccess("修改成功"); }
this.open = false; /** 删除按钮操作 */
this.getList(); function handleDelete(row) {
}); const customerIds = row.customerId || ids.value;
} else { proxy.$modal.confirm('是否确认删除客户主表(mb)编号为"' + customerIds + '"的数据项?').then(function() {
addCustomer(this.form).then(response => { return delCustomer(customerIds);
this.$modal.msgSuccess("新增成功"); }).then(() => {
this.open = false; getList();
this.getList(); proxy.$modal.msgSuccess("删除成功");
}); }).catch(() => {});
} }
} /** 提交按钮 */
}); function submitForm() {
}, proxy.$refs["customerRef"].validate(valid => {
/** 删除按钮操作 */ if (valid) {
handleDelete(row) { form.value.demoGoodsList = demoGoodsList;
const customerIds = row.customerId || this.ids; if (form.value.customerId != null) {
this.$modal.confirm('是否确认删除客户主表(mb)编号为"' + customerIds + '"的数据项?').then(function() { updateCustomer(form.value).then(response => {
return delCustomer(customerIds); proxy.$modal.msgSuccess("修改成功");
}).then(() => { open.value = false;
this.getList(); getList();
this.$modal.msgSuccess("删除成功"); });
}).catch(() => {});
},
/** 商品子序号 */
rowDemoGoodsIndex({ row, rowIndex }) {
row.index = rowIndex + 1;
},
/** 商品子添加按钮操作 */
handleAddDemoGoods() {
let obj = {};
obj.name = "";
obj.weight = "";
obj.price = "";
obj.date = "";
obj.type = "";
this.demoGoodsList.push(obj);
},
/** 商品子删除按钮操作 */
handleDeleteDemoGoods() {
if (this.checkedDemoGoods.length == 0) {
this.$modal.msgError("请先选择要删除的商品子数据");
} else { } else {
const demoGoodsList = this.demoGoodsList; addCustomer(form.value).then(response => {
const checkedDemoGoods = this.checkedDemoGoods; proxy.$modal.msgSuccess("新增成功");
this.demoGoodsList = demoGoodsList.filter(function(item) { open.value = false;
return checkedDemoGoods.indexOf(item.index) == -1 getList();
}); });
} }
},
/** 复选框选中数据 */
handleDemoGoodsSelectionChange(selection) {
this.checkedDemoGoods = selection.map(item => item.index)
},
/** 导出按钮操作 */
handleExport() {
this.download('demo/customer/export', {
...this.queryParams
}, `customer_${new Date().getTime()}.xlsx`)
} }
});
}
/** 商品子序号 */
function rowDemoGoodsIndex({ row, rowIndex }) {
row.index = rowIndex + 1;
}
/** 商品子添加按钮操作 */
function handleAddDemoGoods() {
let obj = {};
obj.name = "";
obj.weight = 1;
obj.price = 0.0;
obj.date = "";
obj.type = "";
demoGoodsList.value.push(obj);
}
/** 商品子删除按钮操作 */
function handleDeleteDemoGoods() {
if (checkedDemoGoods.value.length == 0) {
proxy.$modal.msgError("请先选择要删除的商品子数据");
} else {
const goodsList = demoGoodsList.value;
const checkedGoods = checkedDemoGoods.value;
demoGoodsList.value = goodsList.filter(function(item) {
return checkedGoods.indexOf(item.index) == -1
});
} }
}; }
/** 复选框选中数据 */
function handleDemoGoodsSelectionChange(selection) {
checkedDemoGoods.value = selection.map(item => item.index)
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('demo/customer/export', {
...queryParams.value
}, `customer_${new Date().getTime()}.xlsx`)
}
getList();
</script> </script>

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