前端代码从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
.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:
# 名称
name: RuoYi-Flex
name: Ruoyi-Flex
# 版本
version: 4.1.6
# 版权年份
@ -217,6 +217,7 @@ security:
- /**/*.html
- /**/*.css
- /**/*.js
- /profile/**
# 公共路径
- /favicon.ico
- /error

View File

@ -10,7 +10,6 @@ import org.springframework.stereotype.Component;
*
* @author ruoyi
*/
@Data
@Component
@ConfigurationProperties(prefix = "ruoyi")
public class RuoYiConfig {
@ -43,11 +42,46 @@ public class RuoYiConfig {
/**
* 获取地址开关
*/
@Getter
private static boolean addressEnabled;
public void setAddressEnabled(boolean addressEnabled) {
RuoYiConfig.addressEnabled = addressEnabled;
public String getName()
{
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()
@ -55,6 +89,22 @@ public class RuoYiConfig {
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.WebMvcConfigurer;
import java.util.Arrays;
/**
* 权限安全配置
*
@ -38,12 +40,32 @@ public class SecurityConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
// 注册路由拦截器自定义验证规则
registry.addInterceptor(new SaInterceptor(handler -> {
AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class);
//AllUrlHandler allUrlHandler = SpringUtils.getBean(AllUrlHandler.class);
// 登录验证 -- 排除多个路径
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(() -> {
// 检查是否登录 是否有token
StpUtil.checkLogin();
@ -51,9 +73,9 @@ public class SecurityConfig implements WebMvcConfigurer {
//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.SecurityConfig
com.ruoyi.common.security.handler.GlobalExceptionHandler
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_
# Actuator 监控端点的配置项
spring.application.name: ruoyi-powerjob-server
spring.application.name: Ruoyi-Powerjob-Server
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=ALWAYS
management.endpoint.logfile.external-file=./logs/ruoyi-powerjob-server.log

View File

@ -1,6 +1,6 @@
<template>
<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)
#if($column.query)
#set($dictType=$column.dictType)
@ -17,14 +17,14 @@
v-model="queryParams.${column.javaField}"
placeholder="请输入${comment}"
clearable
@keyup.enter.native="handleQuery"
@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 dict.type.${dictType}"
v-for="dict in ${dictType}"
:key="dict.value"
:label="dict.label"
:value="dict.value"
@ -42,16 +42,15 @@
<el-date-picker clearable
v-model="queryParams.${column.javaField}"
type="date"
value-format="yyyy-MM-dd"
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}">
<el-form-item label="${comment}" style="width: 308px">
<el-date-picker
v-model="daterange${AttrName}"
style="width: 240px"
value-format="yyyy-MM-dd"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
@ -62,8 +61,8 @@
#end
#end
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
@ -72,8 +71,7 @@
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
icon="Plus"
@click="handleAdd"
v-hasPermi="['${moduleName}:${businessName}:add']"
>新增</el-button>
@ -82,12 +80,11 @@
<el-button
type="info"
plain
icon="el-icon-sort"
size="mini"
icon="Sort"
@click="toggleExpandAll"
>展开/折叠</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table
@ -109,23 +106,23 @@
#if($column.pk)
#elseif($column.list && $column.htmlType == "datetime")
<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>
</template>
</el-table-column>
#elseif($column.list && $column.htmlType == "imageUpload")
<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"/>
</template>
</el-table-column>
#elseif($column.list && "" != $column.dictType)
<el-table-column label="${comment}" align="center" prop="${javaField}">
<template slot-scope="scope">
<template #default="scope">
#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
<dict-tag :options="dict.type.${column.dictType}" :value="scope.row.${javaField}"/>
<dict-tag :options="${column.dictType}" :value="scope.row.${javaField}"/>
#end
</template>
</el-table-column>
@ -138,35 +135,17 @@
#end
#end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
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-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 #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" :visible.sync="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<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)
@ -180,7 +159,14 @@
#set($dictType=$column.dictType)
#if("" != $treeParentCode && $column.javaField == $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>
#elseif($column.htmlType == "input")
<el-form-item label="${comment}" prop="${field}">
@ -202,7 +188,7 @@
<el-form-item label="${comment}" prop="${field}">
<el-select v-model="form.${field}" placeholder="请选择${comment}">
<el-option
v-for="dict in dict.type.${dictType}"
v-for="dict in ${dictType}"
:key="dict.value"
:label="dict.label"
#if($column.javaType == "Integer" || $column.javaType == "Long")
@ -223,7 +209,7 @@
<el-form-item label="${comment}" prop="${field}">
<el-checkbox-group v-model="form.${field}">
<el-checkbox
v-for="dict in dict.type.${dictType}"
v-for="dict in ${dictType}"
:key="dict.value"
:label="dict.value">
{{dict.label}}
@ -240,7 +226,7 @@
<el-form-item label="${comment}" prop="${field}">
<el-radio-group v-model="form.${field}">
<el-radio
v-for="dict in dict.type.${dictType}"
v-for="dict in ${dictType}"
:key="dict.value"
#if($column.javaType == "Integer" || $column.javaType == "Long")
:label="parseInt(dict.value)"
@ -261,7 +247,7 @@
<el-date-picker clearable
v-model="form.${field}"
type="date"
value-format="yyyy-MM-dd"
value-format="YYYY-MM-DD"
placeholder="选择${comment}">
</el-date-picker>
</el-form-item>
@ -274,65 +260,59 @@
#end
#end
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
<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>
<script setup name="${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 {
name: "${BusinessName}",
const { proxy } = getCurrentInstance();
#if(${dicts} != '')
dicts: [${dicts}],
#set($dictsNoSymbol=$dicts.replace("'", ""))
const { ${dictsNoSymbol} } = proxy.useDict(${dicts});
#end
components: {
Treeselect
},
data() {
return {
// 遮罩层
loading: true,
// 显示搜索条件
showSearch: true,
// ${functionName}表格数据
${businessName}List: [],
// ${functionName}树选项
${businessName}Options: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 是否展开,默认全部展开
isExpandAll: true,
// 重新渲染表格状态
refreshTable: true,
// 树表表格数据
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)})
// $comment时间范围
daterange${AttrName}: [],
const daterange${AttrName} = ref([]);
#end
#end
// 查询参数
queryParams: {
#foreach ($column in $columns)
const data = reactive({
form: {},
queryParams: {
#foreach ($column in $columns)
#if($column.query)
$column.javaField: null#if($foreach.count != $columns.size()),#end
$column.javaField: null#if($foreach.count != $columns.size()),#end
#end
#end
},
// 表单参数
form: {},
// 表单校验
rules: {
#foreach ($column in $columns)
},
rules: {
#foreach ($column in $columns)
#if($column.required)
#set($parentheseIndex=$column.columnComment.indexOf(""))
#if($parentheseIndex != -1)
@ -340,166 +320,157 @@ export default {
#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
$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
}
};
},
created() {
this.getList();
},
methods: {
/** 查询${functionName}列表 */
getList() {
this.loading = true;
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询${functionName}列表 */
function getList() {
loading.value = true;
#foreach ($column in $columns)
#if($column.htmlType == "datetime" && $column.queryType == "BETWEEN")
this.queryParams.params = {};
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 != this.daterange${AttrName} && '' != this.daterange${AttrName}) {
this.queryParams.params["begin${AttrName}"] = this.daterange${AttrName}[0];
this.queryParams.params["end${AttrName}"] = this.daterange${AttrName}[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}(this.queryParams).then(response => {
this.${businessName}List = this.handleTree(response.data, "${treeCode}", "${treeParentCode}");
this.loading = false;
});
},
/** 转换${functionName}数据结构 */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children;
}
return {
id: node.${treeCode},
label: node.${treeName},
children: node.children
};
},
/** 查询${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 = {
list${BusinessName}(queryParams.value).then(response => {
${businessName}List.value = proxy.handleTree(response.data, "${treeCode}", "${treeParentCode}");
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
$column.javaField: []#if($foreach.count != $columns.size()),#end
#else
$column.javaField: null#if($foreach.count != $columns.size()),#end
$column.javaField: null#if($foreach.count != $columns.size()),#end
#end
#end
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
};
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)})
this.daterange${AttrName} = [];
daterange${AttrName}.value = [];
#end
#end
this.resetForm("queryForm");
this.handleQuery();
},
/** 新增按钮操作 */
handleAdd(row) {
this.reset();
this.getTreeselect();
if (row != null && row.${treeCode}) {
this.form.${treeParentCode} = row.${treeCode};
} else {
this.form.${treeParentCode} = 0;
}
this.open = true;
this.title = "添加${functionName}";
},
/** 展开/折叠操作 */
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(() => {});
}
proxy.resetForm("queryRef");
handleQuery();
}
/** 新增按钮操作 */
async function handleAdd(row) {
reset();
await list${BusinessName}().then(response => {
${businessName}Options.value = proxy.handleTree(response.data, "${treeCode}");
});
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 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>

View File

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

@ -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")
@Log(title = "登录日志", businessType = BusinessType.DELETE)
@DeleteMapping("/{infoIds}")
public AjaxResult remove(@PathVariable Long[] infoIds)
{
return toAjax(logininforService.deleteLogininforByIds(infoIds));

View File

@ -48,7 +48,7 @@ public class SysDeptServiceImpl implements ISysDeptService, DeptService {
@DataScope(deptAlias = "d")
public List<SysDept> selectDeptList(SysDept dept) {
// 只查询未禁用部门
dept.setStatus(UserConstants.DEPT_NORMAL);
//dept.setStatus(UserConstants.DEPT_NORMAL);
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任务调度控制台地址
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任务调度控制台地址
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'
# 若依管理系统/测试环境
VUE_APP_BASE_API = '/stage-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli
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
# 克隆项目
git clone https://gitee.com/y_project/RuoYi-Vue
git clone https://gitee.com/dataprince/ruoyi-flex
# 进入项目目录
cd ruoyi-ui
# 安装依赖
npm install
# 建议不要直接使用 cnpm 安装依赖,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npmmirror.com
# 启动服务
npm run dev
```
浏览器访问 http://localhost:80
前端浏览器访问 http://localhost:80
## 发布

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +1,3 @@
<template>
<div id="app">
<router-view />
<theme-picker />
</div>
<router-view />
</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>
</template>
<script>
export default {
data() {
return {
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]
<script setup>
const route = useRoute();
const router = useRouter();
const levelList = ref([])
if (!this.isDashboard(first)) {
matched = [{ path: '/index', meta: { title: '首页' }}].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
isDashboard(route) {
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)
}
function getBreadcrumb() {
// only show routes with meta.title
let matched = route.matched.filter(item => item.meta && item.meta.title);
const first = matched[0]
//
if (!isDashboard(first)) {
matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched)
}
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>
<style lang="scss" scoped>

View File

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

View File

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

View File

@ -1,7 +1,6 @@
<template>
<div class="upload-file">
<el-upload
multiple
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
@ -12,12 +11,12 @@
:show-file-list="false"
:headers="headers"
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="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">
<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">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
@ -39,158 +38,132 @@
</div>
</template>
<script>
<script setup>
import { getToken } from "@/utils/auth";
export default {
name: "FileUpload",
props: {
//
value: [String, Object, Array],
//
limit: {
type: Number,
default: 5,
},
// (MB)
fileSize: {
type: Number,
default: 5,
},
// , ['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf"],
},
//
isShowTip: {
type: Boolean,
default: true
const props = defineProps({
modelValue: [String, Object, Array],
//
limit: {
type: Number,
default: 5,
},
// (MB)
fileSize: {
type: Number,
default: 5,
},
// , ['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf"],
},
//
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);
}
},
data() {
return {
number: 0,
uploadList: [],
baseUrl: process.env.VUE_APP_BASE_API,
uploadFileUrl: process.env.VUE_APP_BASE_API + "/common/upload", //
headers: {
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) : '';
const isTypeOk = props.fileType.some((type) => {
if (file.type.indexOf(type) > -1) return true;
if (fileExtension && fileExtension.indexOf(type) > -1) return true;
return false;
});
if (!isTypeOk) {
proxy.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
return false;
}
}
};
//
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>
<style scoped lang="scss">

View File

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

View File

@ -1,8 +1,8 @@
<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" />
<el-select
ref="headerSearchSelect"
ref="headerSearchSelectRef"
v-model="search"
:remote-method="querySearch"
filterable
@ -17,138 +17,129 @@
</div>
</template>
<script>
// fuse is a lightweight fuzzy-search module
// make search results more in line with expectations
import Fuse from 'fuse.js/dist/fuse.min.js'
import path from 'path'
<script setup>
import Fuse from 'fuse.js'
import { getNormalPath } from '@/utils/ruoyi'
import { isHttp } from '@/utils/validate'
export default {
name: 'HeaderSearch',
data() {
return {
search: '',
options: [],
searchPool: [],
show: false,
fuse: undefined
const search = ref('');
const options = ref([]);
const searchPool = ref([]);
const show = ref(false);
const fuse = ref(undefined);
const headerSearchSelectRef = ref(null);
const store = useStore();
const router = useRouter();
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: {
routes() {
return this.$store.getters.permission_routes
}
},
watch: {
routes() {
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)
if (r.meta && r.meta.title) {
data.title = [...data.title, r.meta.title]
if (r.redirect !== 'noRedirect') {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
}
}
},
mounted() {
this.searchPool = this.generateRoutes(this.routes)
},
methods: {
click() {
this.show = !this.show
if (this.show) {
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
// recursive child routes
if (r.children) {
const tempRoutes = generateRoutes(r.children, data.path, data.title)
if (tempRoutes.length >= 1) {
res = [...res, ...tempRoutes]
}
},
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>
<style lang="scss" scoped>
<style lang='scss' scoped>
.header-search {
font-size: 0 !important;
@ -168,7 +159,7 @@ export default {
display: inline-block;
vertical-align: middle;
::v-deep .el-input__inner {
:deep(.el-input__inner) {
border-radius: 0;
border: 0;
padding-left: 0;

View File

@ -1,104 +1,74 @@
<!-- @author zhengjie -->
<template>
<div class="icon-body">
<el-input v-model="name" class="icon-search" clearable placeholder="请输入图标名称" @clear="filterIcons" @input="filterIcons">
<i slot="suffix" class="el-icon-search el-input__icon" />
<el-input
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>
<div class="icon-list">
<div class="list-container">
<div v-for="(item, index) in iconList" class="icon-item-wrapper" :key="index" @click="selectedIcon(item)">
<div :class="['icon-item', { active: activeIcon === item }]">
<svg-icon :icon-class="item" class-name="icon" style="height: 25px;width: 16px;"/>
<span>{{ item }}</span>
</div>
</div>
<div v-for="(item, index) in iconList" :key="index" @click="selectedIcon(item)">
<svg-icon :icon-class="item" style="height: 30px;width: 16px;" />
<span>{{ item }}</span>
</div>
</div>
</div>
</template>
<script>
<script setup>
import icons from './requireIcons'
export default {
name: 'IconSelect',
props: {
activeIcon: {
type: String
const iconName = ref('');
const iconList = ref(icons);
const emit = defineEmits(['selected']);
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;
}
},
data() {
return {
name: '',
iconList: icons
}
},
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
span {
display: inline-block;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
}
}
</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,55 +1,20 @@
<template>
<div>
<svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
<svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
</div>
</template>
<script>
import screenfull from 'screenfull'
<script setup>
import { useFullscreen } from '@vueuse/core'
export default {
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)
}
}
}
}
const { isFullscreen, enter, exit, toggle } = useFullscreen();
</script>
<style scoped>
<style lang='scss' scoped>
.screenfull-svg {
display: inline-block;
cursor: pointer;
fill: #5a5e66;;
fill: #5a5e66;
width: 20px;
height: 20px;
vertical-align: 10px;

View File

@ -1,56 +1,57 @@
<template>
<el-dropdown trigger="click" @command="handleSetSize">
<div>
<svg-icon class-name="size-icon" icon-class="size" />
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<div>
<el-dropdown trigger="click" @command="handleSetSize">
<div class="size-icon--style">
<svg-icon class-name="size-icon" icon-class="size" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
{{ item.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script>
export default {
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)
<script setup>
import { ElMessage } from 'element-plus'
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(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
}
}
function refreshView() {
// In order to make the cached page re-rendered
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>
<style lang='scss' scoped>
.size-icon--style {
font-size: 18px;
line-height: 50px;
padding-right: 7px;
}
</style>

View File

@ -1,15 +1,11 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
<svg :class="svgClass" aria-hidden="true">
<use :xlink:href="iconName" :fill="color" />
</svg>
</template>
<script>
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
export default defineComponent({
props: {
iconClass: {
type: String,
@ -18,44 +14,40 @@ export default {
className: {
type: String,
default: ''
}
},
color: {
type: String,
default: ''
},
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
setup(props) {
return {
iconName: computed(() => `#icon-${props.iconClass}`),
svgClass: computed(() => {
if (props.className) {
return `svg-icon ${props.className}`
}
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
})
}
}
}
})
</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 {
width: 1em;
height: 1em;
vertical-align: -0.15em;
position: relative;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
vertical-align: -2px;
}
</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">
<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
>
</template>
<!-- 顶部菜单超出数量折叠 -->
<el-submenu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
<template slot="title">更多菜单</template>
<el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
<template #title>更多菜单</template>
<template v-for="(item, index) in topMenus">
<el-menu-item
: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
>
</template>
</el-submenu>
</el-sub-menu>
</el-menu>
</template>
<script>
import { constantRoutes } from "@/router";
<script setup>
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 {
data() {
return {
//
visibleNumber: 5,
// index
currentIndex: undefined
};
},
computed: {
theme() {
return this.$store.state.settings.theme;
},
//
topMenus() {
let topMenus = [];
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);
const store = useStore();
const route = useRoute();
//
const theme = computed(() => store.state.settings.theme);
//
const routers = computed(() => store.state.permission.topbarRouters);
//
const topMenus = computed(() => {
let topMenus = [];
routers.value.map((menu) => {
if (menu.hidden !== true) {
//
if (menu.path === "/") {
topMenus.push(menu.children[0]);
} else {
//
this.activeRoutes(key);
this.$store.dispatch('app/toggleSideBarHide', false);
topMenus.push(menu);
}
},
//
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>
<style lang="scss">
@ -174,13 +169,13 @@ export default {
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;
color: #303133;
}
/* submenu item */
.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title {
/* sub-menu item */
.topmenu-container.el-menu--horizontal > .el-sub-menu .el-submenu__title {
float: left;
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"
frameborder="no"
style="width: 100%; height: 100%"
scrolling="auto"
/>
scrolling="auto" />
</div>
</template>
<script>
export default {
props: {
src: {
type: String,
default:"/"
}
},
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;";
};
<script setup>
const props = defineProps({
src: {
type: String,
required: true
}
};
})
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>

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 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)
export default function directive(app){
app.directive('hasRole', hasRole)
app.directive('hasPermi', hasPermi)
}
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>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<router-view v-if="!$route.meta.link" :key="key" />
</keep-alive>
</transition>
<iframe-toggle />
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<component :is="Component" :key="route.path"/>
</keep-alive>
</transition>
</router-view>
</section>
</template>
<script>
import iframeToggle from "./IframeToggle/index"
export default {
name: 'AppMain',
components: { iframeToggle },
computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.path
}
}
}
<script setup>
let store = useStore()
const route = useRoute()
store.dispatch('tagsView/addCachedView', route)
const cachedViews = computed(() => {
return store.state.tagsView.cachedViews
})
</script>
<style lang="scss" scoped>
@ -55,21 +48,7 @@ export default {
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.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>

View File

@ -1,24 +1,20 @@
<template>
<transition-group name="fade-transform" mode="out-in">
<inner-link
v-for="(item, index) in iframeViews"
v-for="(item, index) in tagsViewStore.iframeViews"
:key="item.path"
:iframeId="'iframe' + index"
v-show="$route.path === item.path"
v-show="route.path === item.path"
:src="item.meta.link"
></inner-link>
</transition-group>
</template>
<script>
import InnerLink from "../InnerLink/index"
<script setup>
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>

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>
export default {
props: {
src: {
type: String,
default: "/"
},
iframeId: {
type: String
setup() {
const route = useRoute();
const link = route.meta.link;
if (link === "") {
return "404";
}
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>

View File

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

View File

@ -1,260 +1,254 @@
<template>
<el-drawer size="280px" :visible="visible" :with-header="false" :append-to-body="true" :show-close="false">
<div class="drawer-container">
<div>
<div class="setting-drawer-content">
<div class="setting-drawer-title">
<h3 class="drawer-title">主题风格设置</h3>
</div>
<div class="setting-drawer-block-checbox">
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
<img src="@/assets/images/dark.svg" alt="dark">
<div v-if="sideTheme === 'theme-dark'" 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 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>
<el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px">
<div class="setting-drawer-title">
<h3 class="drawer-title">主题风格设置</h3>
</div>
<div class="setting-drawer-block-checbox">
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
<img src="@/assets/images/dark.svg" alt="dark" />
<div v-if="sideTheme === 'theme-dark'" 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>
<el-divider/>
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>开启 TopNav</span>
<el-switch v-model="topNav" class="drawer-switch" />
</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 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 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>
</template>
<script>
import ThemePicker from '@/components/ThemePicker'
<script setup>
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 {
components: { ThemePicker },
data() {
return {
theme: this.$store.state.settings.theme,
sideTheme: this.$store.state.settings.sideTheme
};
},
computed: {
visible: {
get() {
return this.$store.state.settings.showSettings
}
},
fixedHeader: {
get() {
return this.$store.state.settings.fixedHeader
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'fixedHeader',
value: val
})
}
},
topNav: {
get() {
return this.$store.state.settings.topNav
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'topNav',
value: val
})
if (!val) {
this.$store.dispatch('app/toggleSideBarHide', false);
this.$store.commit("SET_SIDEBAR_ROUTERS", this.$store.state.permission.defaultRoutes);
}
}
},
tagsView: {
get() {
return this.$store.state.settings.tagsView
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'tagsView',
value: val
})
}
},
sidebarLogo: {
get() {
return this.$store.state.settings.sidebarLogo
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'sidebarLogo',
value: val
})
}
},
dynamicTitle: {
get() {
return this.$store.state.settings.dynamicTitle
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'dynamicTitle',
value: val
})
}
},
},
methods: {
themeChange(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'theme',
value: val
})
this.theme = val;
},
handleTheme(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'sideTheme',
value: val
})
this.sideTheme = val;
},
saveSetting() {
this.$modal.loading("正在保存到本地,请稍候...");
this.$cache.local.set(
"layout-setting",
`{
"topNav":${this.topNav},
"tagsView":${this.tagsView},
"fixedHeader":${this.fixedHeader},
"sidebarLogo":${this.sidebarLogo},
"dynamicTitle":${this.dynamicTitle},
"sideTheme":"${this.sideTheme}",
"theme":"${this.theme}"
}`
);
setTimeout(this.$modal.closeLoading(), 1000)
},
resetSetting() {
this.$modal.loading("正在清除设置缓存并刷新,请稍候...");
this.$cache.local.remove("layout-setting")
setTimeout("window.location.reload()", 1000)
const { proxy } = getCurrentInstance();
const store = useStore();
const showSettings = ref(false);
const theme = ref(store.state.settings.theme);
const sideTheme = ref(store.state.settings.sideTheme);
const storeSettings = computed(() => store.state.settings);
const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]);
/** 是否需要topnav */
const topNav = computed({
get: () => storeSettings.value.topNav,
set: (val) => {
store.dispatch('settings/changeSetting', {
key: 'topNav',
value: val
})
if (!val) {
store.commit("SET_SIDEBAR_ROUTERS", store.state.permission.defaultRoutes);
}
}
})
/** 是否需要tagview */
const tagsView = computed({
get: () => storeSettings.value.tagsView,
set: (val) => {
store.dispatch('settings/changeSetting', {
key: 'tagsView',
value: val
})
}
})
/**是否需要固定头部 */
const fixedHeader = computed({
get: () => storeSettings.value.fixedHeader,
set: (val) => {
store.dispatch('settings/changeSetting', {
key: 'fixedHeader',
value: val
})
}
})
/**是否需要侧边栏的logo */
const sidebarLogo = computed({
get: () => storeSettings.value.sidebarLogo,
set: (val) => {
store.dispatch('settings/changeSetting', {
key: 'sidebarLogo',
value: val
})
}
})
/**是否需要侧边栏的动态网页的title */
const dynamicTitle = computed({
get: () => storeSettings.value.dynamicTitle,
set: (val) => {
store.dispatch('settings/changeSetting', {
key: 'dynamicTitle',
value: val
})
//
useDynamicTitle()
}
})
function themeChange(val) {
store.dispatch('settings/changeSetting', {
key: 'theme',
value: val
})
theme.value = val;
}
function handleTheme(val) {
store.dispatch('settings/changeSetting', {
key: 'sideTheme',
value: val
})
sideTheme.value = val;
}
function saveSetting() {
proxy.$modal.loading("正在保存到本地,请稍候...");
let layoutSetting = {
"topNav": storeSettings.value.topNav,
"tagsView": storeSettings.value.tagsView,
"fixedHeader": storeSettings.value.fixedHeader,
"sidebarLogo": storeSettings.value.sidebarLogo,
"dynamicTitle": storeSettings.value.dynamicTitle,
"sideTheme": storeSettings.value.sideTheme,
"theme": storeSettings.value.theme
};
localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
setTimeout(proxy.$modal.closeLoading(), 1000)
}
function resetSetting() {
proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
localStorage.removeItem("layout-setting")
setTimeout("window.location.reload()", 1000)
}
function openSetting() {
showSettings.value = true;
}
defineExpose({
openSetting,
})
</script>
<style lang='scss' scoped>
.setting-drawer-title {
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>
.setting-drawer-content {
.setting-drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
line-height: 22px;
font-weight: bold;
}
.drawer-item {
color: rgba(0, 0, 0, 0.65);
padding: 12px 0;
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;
}
.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
}
.comp-style {
float: right;
margin: -3px 8px 0px 0px;
}
}
</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>
<component :is="type" v-bind="linkProps(to)">
<component :is="type" v-bind="linkProps()">
<slot />
</component>
</template>
<script>
<script setup>
import { isExternal } from '@/utils/validate'
export default {
props: {
to: {
type: [String, Object],
required: true
}
},
computed: {
isExternal() {
return isExternal(this.to)
},
type() {
if (this.isExternal) {
return 'a'
}
return 'router-link'
}
},
methods: {
linkProps(to) {
if (this.isExternal) {
return {
href: to,
target: '_blank',
rel: 'noopener'
}
}
return {
to: to
}
const props = defineProps({
to: {
type: [String, Object],
required: true
}
})
const isExt = computed(() => {
return isExternal(props.to)
})
const type = computed(() => {
if (isExt.value) {
return 'a'
}
return 'router-link'
})
function linkProps() {
if (isExt.value) {
return {
href: props.to,
target: '_blank',
rel: 'noopener'
}
}
return {
to: props.to
}
}
</script>

View File

@ -1,45 +1,32 @@
<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">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<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 v-else key="expand" class="sidebar-logo-link" to="/">
<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>
</transition>
</div>
</template>
<script>
import logoImg from '@/assets/logo/logo.png'
import variables from '@/assets/styles/variables.scss'
<script setup>
import variables from '@/assets/styles/variables.module.scss'
import logo from '@/assets/logo/logo.png'
export default {
name: 'SidebarLogo',
props: {
collapse: {
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
}
defineProps({
collapse: {
type: Boolean,
required: true
}
}
})
const title = ref('Ruoyi-Flex管理系统');
const store = useStore();
const sideTheme = computed(() => store.state.settings.sideTheme);
</script>
<style lang="scss" scoped>

View File

@ -1,17 +1,20 @@
<template>
<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)">
<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" />
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
<template #title>{{ onlyOneChild.meta.title }}</template>
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template v-if="item.meta" #title>
<svg-icon :icon-class="item.meta && item.meta.icon" />
<span>{{ item.meta.title }}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
@ -20,81 +23,72 @@
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</el-sub-menu>
</div>
</template>
<script>
import path from 'path'
<script setup>
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
import { getNormalPath } from '@/utils/ruoyi'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
const props = defineProps({
// route object
item: {
type: Object,
required: true
},
data() {
this.onlyOneChild = null
return {}
isNest: {
type: Boolean,
default: false
},
methods: {
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)
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)
}
basePath: {
type: String,
default: ''
}
})
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>

View File

@ -1,57 +1,49 @@
<template>
<div :class="{'has-logo':showLogo}" :style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
:text-color="settings.sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
:unique-opened="true"
:active-text-color="settings.theme"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item
v-for="(route, index) in sidebarRouters"
:key="route.path + index"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
</div>
<div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
:text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
:unique-opened="true"
:active-text-color="theme"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item
v-for="(route, index) in sidebarRouters"
:key="route.path + index"
:item="route"
:base-path="route.path"
/>
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters, mapState } from "vuex";
import Logo from "./Logo";
import SidebarItem from "./SidebarItem";
import variables from "@/assets/styles/variables.scss";
<script setup>
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/assets/styles/variables.module.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>

View File

@ -1,94 +1,112 @@
<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 />
</el-scrollbar>
</template>
<script>
const tagAndTagSpacing = 4 // tagAndTagSpacing
<script setup>
import {
ref,
getCurrentInstance,
computed,
onMounted,
onBeforeUnmount,
} from 'vue';
import {useStore} from "vuex";
export default {
name: 'ScrollPane',
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
const tagAndTagSpacing = ref(4);
const { proxy } = getCurrentInstance();
let firstTag = null
let lastTag = null
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef);
// find first tag and last tag
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
onMounted(() => {
scrollWrapper.value.addEventListener('scroll', emitScroll, true)
})
onBeforeUnmount(() => {
scrollWrapper.value.removeEventListener('scroll', emitScroll)
})
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
// find preTag and nextTag
const currentIndex = tagList.findIndex(item => item === currentTag)
const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1]
function handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = scrollWrapper.value;
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
}
const emits = defineEmits()
const emitScroll = () => {
emits('scroll')
}
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
const store = useStore();
const visitedViews = computed(() => store.state.tagsView.visitedViews);
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
function moveToTarget(currentTag) {
const $container = proxy.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = scrollWrapper.value;
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
let firstTag = null
let lastTag = null
// 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>
<style lang="scss" scoped>
<style lang='scss' scoped>
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
::v-deep {
.el-scrollbar__bar {
bottom: 0px;
}
.el-scrollbar__wrap {
height: 49px;
}
:deep(.el-scrollbar__bar) {
bottom: 0px;
}
:deep(.el-scrollbar__wrap) {
height: 49px;
}
}
</style>

View File

@ -1,249 +1,245 @@
<template>
<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
v-for="tag in visitedViews"
ref="tag"
: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 }"
tag="span"
class="tags-view-item"
:style="activeStyle(tag)"
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
@contextmenu.prevent.native="openMenu(tag,$event)"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
{{ 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>
</scroll-pane>
<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 v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><i class="el-icon-close"></i> 关闭当前</li>
<li @click="closeOthersTags"><i class="el-icon-circle-close"></i> 关闭其他</li>
<li v-if="!isFirstView()" @click="closeLeftTags"><i class="el-icon-back"></i> 关闭左侧</li>
<li v-if="!isLastView()" @click="closeRightTags"><i class="el-icon-right"></i> 关闭右侧</li>
<li @click="closeAllTags(selectedTag)"><i class="el-icon-circle-close"></i> 全部关闭</li>
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">
<refresh-right style="width: 1em; height: 1em;" /> 刷新页面
</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
<close style="width: 1em; height: 1em;" /> 关闭当前
</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>
</div>
</template>
<script>
<script setup>
import ScrollPane from './ScrollPane'
import path from 'path'
import { getNormalPath } from '@/utils/ruoyi'
export default {
components: { ScrollPane },
data() {
return {
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: []
const visible = ref(false);
const top = ref(0);
const left = ref(0);
const selectedTag = ref({});
const affixTags = ref([]);
const scrollPaneRef = ref(null);
const { proxy } = getCurrentInstance();
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 }
})
}
},
computed: {
visitedViews() {
return this.$store.state.tagsView.visitedViews
},
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)
if (route.children) {
const tempTags = filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
},
mounted() {
this.initTags()
this.addTags()
},
methods: {
isActive(route) {
return route.path === this.$route.path
},
activeStyle(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()
})
return tags
}
function initTags() {
const res = filterAffixTags(routes.value);
affixTags.value = res;
for (const tag of res) {
// Must have tag name
if (tag.name) {
store.dispatch('tagsView/addVisitedView', tag)
}
}
}
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>
<style lang="scss" scoped>
<style lang='scss' scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
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-item {
display: inline-block;
@ -269,7 +265,7 @@ export default {
color: #fff;
border-color: #42b983;
&::before {
content: '';
content: "";
background: #fff;
display: inline-block;
width: 8px;
@ -292,7 +288,7 @@ export default {
font-size: 12px;
font-weight: 400;
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 {
margin: 0;
padding: 7px 16px;
@ -315,16 +311,18 @@ export default {
vertical-align: 2px;
border-radius: 50%;
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%;
&:before {
transform: scale(.6);
transform: scale(0.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
width: 12px !important;
height: 12px !important;
}
}
}

View File

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

View File

@ -1,111 +1,105 @@
<template>
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar v-if="!sidebar.hide" class="sidebar-container"/>
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar/>
<tags-view v-if="needTagsView"/>
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }">
<navbar @setLayout="setLayout" />
<tags-view v-if="needTagsView" />
</div>
<app-main/>
<right-panel>
<settings/>
</right-panel>
<app-main />
<settings ref="settingRef" />
</div>
</div>
</template>
<script>
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
import variables from '@/assets/styles/variables.scss'
<script setup>
import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
import defaultSettings from '@/settings'
export default {
name: 'Layout',
components: {
AppMain,
Navbar,
RightPanel,
Settings,
Sidebar,
TagsView
},
mixins: [ResizeMixin],
computed: {
...mapState({
theme: state => state.settings.theme,
sideTheme: state => state.settings.sideTheme,
sidebar: state => state.app.sidebar,
device: state => state.app.device,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader
}),
classObj() {
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 })
}
const store = useStore();
const theme = computed(() => store.state.settings.theme);
const sideTheme = computed(() => store.state.settings.sideTheme);
const sidebar = computed(() => store.state.app.sidebar);
const device = computed(() => store.state.app.device);
const needTagsView = computed(() => store.state.settings.tagsView);
const fixedHeader = computed(() => store.state.settings.fixedHeader);
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened,
openSidebar: sidebar.value.opened,
withoutAnimation: sidebar.value.withoutAnimation,
mobile: device.value === 'mobile'
}))
const { width, height } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design
watchEffect(() => {
if (device.value === 'mobile' && sidebar.value.opened) {
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>
<style lang="scss" scoped>
@import "~@/assets/styles/mixin.scss";
@import "~@/assets/styles/variables.scss";
@import "@/assets/styles/mixin.scss";
@import "@/assets/styles/variables.module.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
&.mobile.openSidebar {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
}
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.sidebarHide .fixed-header {
width: 100%;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
transition: width 0.28s;
}
.mobile .fixed-header {
width: 100%;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px);
}
.mobile .fixed-header {
width: 100%;
}
</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 Element from 'element-ui'
import './assets/styles/element-variables.scss'
import ElementPlus from 'element-plus'
import locale from 'element-plus/lib/locale/lang/zh-cn' // 中文语言
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 store from './store'
import router from './router'
import directive from './directive' // directive
// 注册指令
import plugins from './plugins' // plugins
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 { getDicts } from "@/api/system/dict/data";
import { getConfigKey } from "@/api/system/config";
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi";
import { useDict } from '@/utils/dict'
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 Editor from "@/components/Editor"
import RightToolbar from '@/components/RightToolbar'
// 文件上传组件
import FileUpload from "@/components/FileUpload"
// 图片上传组件
import ImageUpload from "@/components/ImageUpload"
// 图片预览组件
import ImagePreview from "@/components/ImagePreview"
// 自定义树选择组件
import TreeSelect from '@/components/TreeSelect'
// 字典标签组件
import DictTag from '@/components/DictTag'
// 头部标签组件
import VueMeta from 'vue-meta'
// 字典数据组件
import DictData from '@/components/DictData'
const app = createApp(App)
// 全局方法挂载
Vue.prototype.getDicts = getDicts
Vue.prototype.getConfigKey = getConfigKey
Vue.prototype.parseTime = parseTime
Vue.prototype.resetForm = resetForm
Vue.prototype.addDateRange = addDateRange
Vue.prototype.selectDictLabel = selectDictLabel
Vue.prototype.selectDictLabels = selectDictLabels
Vue.prototype.download = download
Vue.prototype.handleTree = handleTree
app.config.globalProperties.useDict = useDict
app.config.globalProperties.download = download
app.config.globalProperties.parseTime = parseTime
app.config.globalProperties.resetForm = resetForm
app.config.globalProperties.handleTree = handleTree
app.config.globalProperties.addDateRange = addDateRange
app.config.globalProperties.selectDictLabel = selectDictLabel
// 全局组件挂载
Vue.component('DictTag', DictTag)
Vue.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar)
Vue.component('Editor', Editor)
Vue.component('FileUpload', FileUpload)
Vue.component('ImageUpload', ImageUpload)
Vue.component('ImagePreview', ImagePreview)
app.component('DictTag', DictTag)
app.component('Pagination', Pagination)
app.component('TreeSelect', TreeSelect)
app.component('FileUpload', FileUpload)
app.component('ImageUpload', ImageUpload)
app.component('ImagePreview', ImagePreview)
app.component('RightToolbar', RightToolbar)
Vue.use(directive)
Vue.use(plugins)
Vue.use(VueMeta)
DictData.install()
app.use(router)
app.use(store)
app.use(plugins)
app.use(elementIcons)
//app.component('svg-icon', SvgIcon)
app.component('SvgIcon', SvgIcon)
/**
* 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! ! !
*/
directive(app)
Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
// 使用element-plus 并且设置全局的大小
app.use(ElementPlus, {
locale: locale,
// 支持 large、default、small
size: Cookies.get('size') || 'default'
})
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
app.mount('#app')

View File

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

View File

@ -1,42 +1,42 @@
import axios from 'axios'
import { Message } from 'element-ui'
import { ElMessage } from 'element-plus'
import { saveAs } from 'file-saver'
import { getToken } from '@/utils/auth'
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 {
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({
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then((res) => {
const isBlob = blobValidate(res.data);
if (isBlob) {
}).then(async (res) => {
const isLogin = await blobValidate(res.data);
if (isLogin) {
const blob = new Blob([res.data])
this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
this.saveAs(blob, decodeURI(res.headers['download-filename']))
} else {
this.printErrMsg(res.data);
}
})
},
resource(resource) {
var url = baseURL + "/common/download/resource?resource=" + encodeURIComponent(resource);
var url = baseURL + "/common/download/resource?resource=" + encodeURI(resource);
axios({
method: 'get',
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then((res) => {
const isBlob = blobValidate(res.data);
if (isBlob) {
}).then(async (res) => {
const isLogin = await blobValidate(res.data);
if (isLogin) {
const blob = new Blob([res.data])
this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
this.saveAs(blob, decodeURI(res.headers['download-filename']))
} else {
this.printErrMsg(res.data);
}
@ -49,9 +49,9 @@ export default {
url: url,
responseType: 'blob',
headers: { 'Authorization': 'Bearer ' + getToken() }
}).then((res) => {
const isBlob = blobValidate(res.data);
if (isBlob) {
}).then(async (res) => {
const isLogin = await blobValidate(res.data);
if (isLogin) {
const blob = new Blob([res.data], { type: 'application/zip' })
this.saveAs(blob, name)
} else {
@ -66,7 +66,7 @@ export default {
const resText = await data.text();
const rspObj = JSON.parse(resText);
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 download from './download'
export default {
install(Vue) {
// 页签操作
Vue.prototype.$tab = tab
// 认证对象
Vue.prototype.$auth = auth
// 缓存对象
Vue.prototype.$cache = cache
// 模态框对象
Vue.prototype.$modal = modal
// 下载文件
Vue.prototype.$download = download
}
export default function installPlugins(app){
// 页签操作
app.config.globalProperties.$tab = tab
// 认证对象
app.config.globalProperties.$auth = auth
// 缓存对象
app.config.globalProperties.$cache = cache
// 模态框对象
app.config.globalProperties.$modal = modal
// 下载文件
app.config.globalProperties.$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;
export default {
// 消息提示
msg(content) {
Message.info(content)
ElMessage.info(content)
},
// 错误消息
msgError(content) {
Message.error(content)
ElMessage.error(content)
},
// 成功消息
msgSuccess(content) {
Message.success(content)
ElMessage.success(content)
},
// 警告消息
msgWarning(content) {
Message.warning(content)
ElMessage.warning(content)
},
// 弹出提示
alert(content) {
MessageBox.alert(content, "系统提示")
ElMessageBox.alert(content, "系统提示")
},
// 错误提示
alertError(content) {
MessageBox.alert(content, "系统提示", { type: 'error' })
ElMessageBox.alert(content, "系统提示", { type: 'error' })
},
// 成功提示
alertSuccess(content) {
MessageBox.alert(content, "系统提示", { type: 'success' })
ElMessageBox.alert(content, "系统提示", { type: 'success' })
},
// 警告提示
alertWarning(content) {
MessageBox.alert(content, "系统提示", { type: 'warning' })
ElMessageBox.alert(content, "系统提示", { type: 'warning' })
},
// 通知提示
notify(content) {
Notification.info(content)
ElNotification.info(content)
},
// 错误通知
notifyError(content) {
Notification.error(content);
ElNotification.error(content);
},
// 成功通知
notifySuccess(content) {
Notification.success(content)
ElNotification.success(content)
},
// 警告通知
notifyWarning(content) {
Notification.warning(content)
ElNotification.warning(content)
},
// 确认窗体
confirm(content) {
return MessageBox.confirm(content, "系统提示", {
return ElMessageBox.confirm(content, "系统提示", {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: "warning",
@ -61,7 +61,7 @@ export default {
},
// 提交内容
prompt(content) {
return MessageBox.prompt(content, "系统提示", {
return ElMessageBox.prompt(content, "系统提示", {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: "warning",
@ -69,10 +69,9 @@ export default {
},
// 打开遮罩层
loading(content) {
loadingInstance = Loading.service({
loadingInstance = ElLoading.service({
lock: true,
text: content,
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.7)",
})
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import defAva from '@/assets/images/profile.jpg'
const user = {
state: {
@ -52,7 +53,7 @@ const user = {
return new Promise((resolve, reject) => {
getInfo().then(res => {
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是否是一个非空数组
commit('SET_ROLES', res.roles)
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) {
if (cellValue == null || cellValue == "") return "";
var date = new Date(cellValue)
var date = new Date(cellValue)
var year = date.getFullYear()
var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
}
@ -330,7 +330,7 @@ export function makeMap(str, expectsLowerCase) {
? val => map[val.toLowerCase()]
: val => map[val]
}
export const exportDefault = 'export default '
export const beautifierConf = {
@ -387,4 +387,4 @@ export function camelCase(str) {
export function isNumberStr(str) {
return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
}

View File

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

View File

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

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