8 Commits

Author SHA1 Message Date
RuoYi
8c49d541ba 用户密码支持自定义配置规则 2026-04-17 14:48:04 +08:00
RuoYi
91185005ce 优化代码生成同步操作column_type没更新问题 2026-04-16 14:25:47 +08:00
RuoYi
8dd8bc784a 优化白名单支持对通配符路径匹配 2026-04-16 13:44:10 +08:00
RuoYi
bcdb182129 通知公告新增阅读用户&详细 2026-04-14 16:44:04 +08:00
RuoYi
2bffc42d19 新增标签页样式chrome风格 2026-04-13 10:11:12 +08:00
RuoYi
a1e7704286 新增代码生成详情页功能 2026-04-12 10:42:38 +08:00
RuoYi
ad242d9327 自动导入配置 2026-04-10 16:37:54 +08:00
RuoYi
19748abe4b 代码生成修改拖拽时显示手指样式 2026-04-10 16:36:42 +08:00
40 changed files with 1419 additions and 182 deletions

View File

@@ -31,6 +31,9 @@ public class GenConstants
/** 上级菜单名称字段 */
public static final String PARENT_MENU_NAME = "parentMenuName";
/** 生成详情页开关 */
public static final String GEN_VIEW = "genView";
/** 数据库字符串类型 */
public static final String[] COLUMNTYPE_STR = { "char", "varchar", "nvarchar", "varchar2" };

View File

@@ -101,6 +101,9 @@ public class GenTable extends BaseEntity
/** 上级菜单名称字段 */
private String parentMenuName;
/** 是否生成详情页 */
private boolean isView;
public Long getTableId()
{
return tableId;
@@ -351,6 +354,16 @@ public class GenTable extends BaseEntity
this.parentMenuName = parentMenuName;
}
public boolean isView()
{
return isView;
}
public void setView(boolean isView)
{
this.isView = isView;
}
public boolean isSub()
{
return isSub(this.tplCategory);

View File

@@ -209,7 +209,7 @@ public class GenTableServiceImpl implements IGenTableService
VelocityContext context = VelocityUtils.prepareContext(table);
// 获取模板列表
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType());
List<String> templates = VelocityUtils.getTemplateList(table);
for (String template : templates)
{
// 渲染模板
@@ -253,10 +253,10 @@ public class GenTableServiceImpl implements IGenTableService
VelocityContext context = VelocityUtils.prepareContext(table);
// 获取模板列表
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType());
List<String> templates = VelocityUtils.getTemplateList(table);
for (String template : templates)
{
if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "index.vue.vm", "index-tree.vue.vm"))
if (!StringUtils.containsAny(template, "sql.vm", "api.js.vm", "api.ts.vm", "type.ts.vm", "index.ts.vm", "index.vue.vm", "index-tree.vue.vm", "view.vue.vm"))
{
// 渲染模板
StringWriter sw = new StringWriter();
@@ -371,7 +371,7 @@ public class GenTableServiceImpl implements IGenTableService
VelocityContext context = VelocityUtils.prepareContext(table);
// 获取模板列表
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory(), table.getTplWebType());
List<String> templates = VelocityUtils.getTemplateList(table);
for (String template : templates)
{
// 渲染模板
@@ -524,12 +524,14 @@ public class GenTableServiceImpl implements IGenTableService
String treeName = paramsObj.getString(GenConstants.TREE_NAME);
Long parentMenuId = paramsObj.getLongValue(GenConstants.PARENT_MENU_ID);
String parentMenuName = paramsObj.getString(GenConstants.PARENT_MENU_NAME);
boolean isView = paramsObj.getBooleanValue(GenConstants.GEN_VIEW);
genTable.setTreeCode(treeCode);
genTable.setTreeParentCode(treeParentCode);
genTable.setTreeName(treeName);
genTable.setParentMenuId(parentMenuId);
genTable.setParentMenuName(parentMenuName);
genTable.setView(isView);
}
}

View File

@@ -14,7 +14,7 @@ import com.ruoyi.gen.domain.GenTable;
import com.ruoyi.gen.domain.GenTableColumn;
/**
* 模板工具类
* 模板处理工具类
*
* @author ruoyi
*/
@@ -68,6 +68,7 @@ public class VelocityUtils
velocityContext.put("columns", genTable.getColumns());
velocityContext.put("table", genTable);
velocityContext.put("dicts", getDicts(genTable));
setExtensionsContext(velocityContext, genTable.getOptions());
setMenuVelocityContext(velocityContext, genTable);
if (GenConstants.TPL_TREE.equals(tplCategory))
{
@@ -80,6 +81,13 @@ public class VelocityUtils
return velocityContext;
}
public static void setExtensionsContext(VelocityContext context, String options)
{
JSONObject paramsObj = JSONObject.parseObject(options);
boolean genView = genView(paramsObj);
context.put("genView", genView);
}
public static void setMenuVelocityContext(VelocityContext context, GenTable genTable)
{
String options = genTable.getOptions();
@@ -134,8 +142,12 @@ public class VelocityUtils
* @param tplWebType 前端类型
* @return 模板列表
*/
public static List<String> getTemplateList(String tplCategory, String tplWebType)
public static List<String> getTemplateList(GenTable table)
{
String tplWebType = table.getTplWebType();
String tplCategory = table.getTplCategory();
JSONObject paramsObj = JSONObject.parseObject(table.getOptions());
boolean isView = genView(paramsObj);
String useWebType = "vm/vue";
String apiTemplate = "vm/js/api.js.vm";
if (StringUtils.equals(ELEMENT_PLUS, tplWebType))
@@ -174,6 +186,10 @@ public class VelocityUtils
templates.add(useWebType + "/index.vue.vm");
templates.add("vm/java/sub-domain.java.vm");
}
if (isView)
{
templates.add(useWebType + "/view.vue.vm");
}
return templates;
}
@@ -253,6 +269,10 @@ public class VelocityUtils
{
fileName = StringUtils.format("{}/views/{}/{}/index.vue", vuePath, moduleName, businessName);
}
else if (template.contains("view.vue.vm"))
{
fileName = StringUtils.format("{}/views/{}/{}/view.vue", vuePath, moduleName, businessName);
}
return fileName;
}
@@ -394,6 +414,21 @@ public class VelocityUtils
return StringUtils.EMPTY;
}
/**
* 扩展功能/生成详情页
*
* @param paramsObj 生成其他选项
* @return 是否生成详细页
*/
public static boolean genView(JSONObject paramsObj)
{
if (StringUtils.isNotNull(paramsObj) && paramsObj.containsKey(GenConstants.GEN_VIEW))
{
return paramsObj.getBoolean(GenConstants.GEN_VIEW);
}
return false;
}
/**
* 获取树名称
*

View File

@@ -94,6 +94,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<set>
<if test="columnComment != null">column_comment = #{columnComment},</if>
<if test="javaType != null">java_type = #{javaType},</if>
<if test="columnType != null">column_type = #{columnType},</if>
<if test="javaField != null">java_field = #{javaField},</if>
<if test="isInsert != null">is_insert = #{isInsert},</if>
<if test="isEdit != null">is_edit = #{isEdit},</if>

View File

@@ -139,6 +139,15 @@
#end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
#if($genView)
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleViewData(scope.row)"
v-hasPermi="['${permissionPrefix}:query']"
>详情</el-button>
#end
<el-button
size="mini"
type="text"
@@ -164,6 +173,10 @@
</el-table-column>
</el-table>
#if($genView)
<!-- ${functionName}详情抽屉 -->
<${businessName}-view-drawer ref="${businessName}ViewRef" />
#end
<!-- 添加或修改${functionName}对话框 -->
#if($table.formColNum == 2)
#set($dialogWidth = "800px")
@@ -319,6 +332,9 @@
<script>
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}"
#if($genView)
import ${BusinessName}ViewDrawer from "./view"
#end
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
@@ -328,6 +344,9 @@ export default {
dicts: [${dicts}],
#end
components: {
#if($genView)
${BusinessName}ViewDrawer,
#end
Treeselect
},
data() {
@@ -483,6 +502,12 @@ export default {
this.refreshTable = true
})
},
#if($genView)
/** 详情按钮操作 */
handleViewData(row) {
this.#[[$]]#refs["${businessName}ViewRef"].open(row.${pkColumn.javaField})
},
#end
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()

View File

@@ -153,6 +153,15 @@
#end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
#if($genView)
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleViewData(scope.row)"
v-hasPermi="['${permissionPrefix}:query']"
>详情</el-button>
#end
<el-button
size="mini"
type="text"
@@ -179,6 +188,10 @@
@pagination="getList"
/>
#if($genView)
<!-- ${functionName}详情抽屉 -->
<${businessName}-view-drawer ref="${businessName}ViewRef" />
#end
<!-- 添加或修改${functionName}对话框 -->
#if($table.formColNum == 2)
#set($dialogWidth = "800px")
@@ -387,9 +400,15 @@
<script>
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}"
#if($genView)
import ${BusinessName}ViewDrawer from "./view"
#end
export default {
name: "${BusinessName}",
#if($genView)
components: { ${BusinessName}ViewDrawer },
#end
#if(${dicts} != '')
dicts: [${dicts}],
#end
@@ -623,6 +642,12 @@ export default {
handle${subClassName}SelectionChange(selection) {
this.checked${subClassName} = selection.map(item => item.index)
},
#end
#if($genView)
/** 详情按钮操作 */
handleViewData(row) {
this.#[[$]]#refs["${businessName}ViewRef"].open(row.${pkColumn.javaField})
},
#end
/** 导出按钮操作 */
handleExport() {

View File

@@ -136,6 +136,9 @@
#end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
#if($genView)
<el-button link type="primary" icon="View" @click="handleViewData(scope.row)" v-hasPermi="['${permissionPrefix}:query']">详情</el-button>
#end
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${permissionPrefix}:edit']">修改</el-button>
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['${permissionPrefix}:add']">新增</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${permissionPrefix}:remove']">删除</el-button>
@@ -143,6 +146,10 @@
</el-table-column>
</el-table>
#if($genView)
<!-- ${functionName}详情抽屉 -->
<${businessName}-view-drawer ref="${businessName}ViewRef" />
#end
<!-- 添加或修改${functionName}对话框 -->
#if($table.formColNum == 2)
#set($dialogWidth = "800px")
@@ -307,11 +314,14 @@
<script setup name="${BusinessName}">
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}"
#if($genView)
import ${BusinessName}ViewDrawer from "./view"
#end
const { proxy } = getCurrentInstance()
#if(${dicts} != '')
#set($dictsNoSymbol=$dicts.replace("'", ""))
const { ${dictsNoSymbol} } = proxy.useDict(${dicts})
const { ${dictsNoSymbol} } = useDict(${dicts})
#end
const ${businessName}List = ref([])
@@ -334,7 +344,7 @@ const data = reactive({
queryParams: {
#foreach ($column in $columns)
#if($column.query)
$column.javaField: null#if($foreach.count != $columns.size()),#end
$column.javaField: undefined#if($foreach.count != $columns.size()),#end
#end
#end
},
@@ -391,13 +401,13 @@ function getTreeselect() {
})
}
// 取消按钮
/** 取消按钮 */
function cancel() {
open.value = false
reset()
}
// 表单重置
/** 表单重置 */
function reset() {
form.value = {
#foreach ($column in $columns)
@@ -449,6 +459,13 @@ function toggleExpandAll() {
refreshTable.value = true
})
}
#if($genView)
/** 详情按钮操作 */
function handleViewData(row) {
proxy.#[[$]]#refs["${businessName}ViewRef"].open(row.${pkColumn.javaField})
}
#end
/** 修改按钮操作 */
async function handleUpdate(row) {

View File

@@ -148,6 +148,9 @@
#end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
#if($genView)
<el-button link type="primary" icon="View" @click="handleViewData(scope.row)" v-hasPermi="['${permissionPrefix}:query']">详情</el-button>
#end
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${permissionPrefix}:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${permissionPrefix}:remove']">删除</el-button>
</template>
@@ -162,6 +165,10 @@
@pagination="getList"
/>
#if($genView)
<!-- ${functionName}详情抽屉 -->
<${businessName}-view-drawer ref="${businessName}ViewRef" />
#end
<!-- 添加或修改${functionName}对话框 -->
#if($table.formColNum == 2)
#set($dialogWidth = "800px")
@@ -381,11 +388,14 @@
<script setup name="${BusinessName}">
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}"
#if($genView)
import ${BusinessName}ViewDrawer from "./view"
#end
const { proxy } = getCurrentInstance()
#if(${dicts} != '')
#set($dictsNoSymbol=$dicts.replace("'", ""))
const { ${dictsNoSymbol} } = proxy.useDict(${dicts})
const { ${dictsNoSymbol} } = useDict(${dicts})
#end
const ${businessName}List = ref([])
@@ -417,7 +427,7 @@ const data = reactive({
pageSize: 10,
#foreach ($column in $columns)
#if($column.query)
$column.javaField: null#if($foreach.count != $columns.size()),#end
$column.javaField: undefined#if($foreach.count != $columns.size()),#end
#end
#end
},
@@ -465,13 +475,13 @@ function getList() {
})
}
// 取消按钮
/** 取消按钮 */
function cancel() {
open.value = false
reset()
}
// 表单重置
/** 表单重置 */
function reset() {
form.value = {
#foreach ($column in $columns)
@@ -506,7 +516,7 @@ function resetQuery() {
handleQuery()
}
// 多选框选中数据
/** 多选框选中数据 */
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.${pkColumn.javaField})
single.value = selection.length != 1
@@ -586,7 +596,7 @@ function handleAdd${subClassName}() {
#foreach($column in $subTable.columns)
#if($column.pk || $column.javaField == ${subTableFkclassName})
#elseif($column.list && "" != $javaField)
obj.$column.javaField = ""
obj.$column.javaField = undefined
#end
#end
${subclassName}List.value.push(obj)
@@ -610,6 +620,13 @@ function handle${subClassName}SelectionChange(selection) {
checked${subClassName}.value = selection.map(item => item.index)
}
#end
#if($genView)
/** 详情按钮操作 */
function handleViewData(row) {
proxy.#[[$]]#refs["${businessName}ViewRef"].open(row.${pkColumn.javaField})
}
#end
/** 导出按钮操作 */
function handleExport() {

View File

@@ -0,0 +1,83 @@
<template>
<el-drawer title="${functionName}详情" v-model="visible" direction="rtl" size="60%" append-to-body :before-close="handleClose" class="detail-drawer">
<div v-loading="loading" class="drawer-content">
<h4 class="section-header">基本信息</h4>
#set($i = 0)
#foreach($column in $columns)
#if(!$column.pk && $column.list)
#set($dictType=$column.dictType)
#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($i % 2 == 0)
<el-row :gutter="20" class="mb8">
#end
<el-col :span="12">
<div class="info-item">
<label class="info-label">${comment}</label>
<span class="info-value plaintext">
#if("" != $dictType)
#if($column.htmlType == "checkbox")
<dict-tag :options="${dictType}" :value="info.${javaField} ? info.${javaField}.split(',') : []" />
#else
<dict-tag :options="${dictType}" :value="info.${javaField}" />
#end
#elseif($column.htmlType == "datetime")
{{ parseTime(info.${javaField}, '{y}-{m}-{d}') }}
#elseif($column.htmlType == "imageUpload")
<image-preview :src="info.${javaField}" :width="60" :height="60" />
#else
{{ info.${javaField} }}
#end
</span>
</div>
</el-col>
#set($i = $i + 1)
#if($i % 2 == 0)
</el-row>
#end
#end
#end
#if($i % 2 != 0)
</el-row>
#end
</div>
</el-drawer>
</template>
<script setup name="${BusinessName}ViewDrawer">
import { get${BusinessName} } from '@/api/${moduleName}/${businessName}'
#if(${dicts} != '')
#set($dictsNoSymbol=$dicts.replace("'", ""))
const { ${dictsNoSymbol} } = useDict(${dicts})
#end
const visible = ref(false)
const loading = ref(false)
const info = reactive({})
const open = async (${pkColumn.javaField}) => {
visible.value = true
loading.value = true
try {
const res = await get${BusinessName}(${pkColumn.javaField})
Object.assign(info, res.data || {})
} catch (error) {
console.error('获取${functionName}信息失败:', error)
} finally {
loading.value = false
}
}
function handleClose() {
visible.value = false
Object.keys(info).forEach(key => delete info[key])
}
defineExpose({ open })
</script>

View File

@@ -136,6 +136,9 @@
#end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
#if($genView)
<el-button link type="primary" icon="View" @click="handleViewData(scope.row)" v-hasPermi="['${permissionPrefix}:query']">详情</el-button>
#end
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${permissionPrefix}:edit']">修改</el-button>
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['${permissionPrefix}:add']">新增</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${permissionPrefix}:remove']">删除</el-button>
@@ -143,6 +146,10 @@
</el-table-column>
</el-table>
#if($genView)
<!-- ${functionName}详情抽屉 -->
<${businessName}-view-drawer ref="${businessName}ViewRef" />
#end
<!-- 添加或修改${functionName}对话框 -->
#if($table.formColNum == 2)
#set($dialogWidth = "800px")
@@ -307,13 +314,16 @@
<script setup lang="ts" name="${BusinessName}">
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}"
#if($genView)
import ${BusinessName}ViewDrawer from "./view"
#end
import type { ${ClassName}, ${BusinessName}QueryParams } from "@/types/api/${moduleName}/${businessName}"
import type { TreeSelect } from '@/types/api/common'
const { proxy } = getCurrentInstance()
#if(${dicts} != '')
#set($dictsNoSymbol=$dicts.replace("'", ""))
const { ${dictsNoSymbol} } = proxy.useDict(${dicts})
const { ${dictsNoSymbol} } = useDict(${dicts})
#end
const ${businessName}List = ref<any[]>([])
@@ -393,13 +403,13 @@ function getTreeselect() {
})
}
// 取消按钮
/** 取消按钮 */
function cancel() {
open.value = false
reset()
}
// 表单重置
/** 表单重置 */
function reset() {
form.value = {
#foreach ($column in $columns)
@@ -451,6 +461,13 @@ function toggleExpandAll() {
refreshTable.value = true
})
}
#if($genView)
/** 详情按钮操作 */
function handleViewData(row: ${ClassName}) {
proxy.#[[$]]#refs["${businessName}ViewRef"].open(row.${pkColumn.javaField})
}
#end
/** 修改按钮操作 */
async function handleUpdate(row: ${ClassName}) {

View File

@@ -148,6 +148,9 @@
#end
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
#if($genView)
<el-button link type="primary" icon="View" @click="handleViewData(scope.row)" v-hasPermi="['${permissionPrefix}:query']">详情</el-button>
#end
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${permissionPrefix}:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['${permissionPrefix}:remove']">删除</el-button>
</template>
@@ -162,6 +165,10 @@
@pagination="getList"
/>
#if($genView)
<!-- ${functionName}详情抽屉 -->
<${businessName}-view-drawer ref="${businessName}ViewRef" />
#end
<!-- 添加或修改${functionName}对话框 -->
#if($table.formColNum == 2)
#set($dialogWidth = "800px")
@@ -379,18 +386,21 @@
</div>
</template>
<script setup lang="ts" name="Config">
<script setup lang="ts" name="${BusinessName}">
#if($table.sub)
import type { ${ClassName}, ${subClassName}, ${BusinessName}QueryParams } from "@/types/api/${moduleName}/${businessName}"
#else
import type { ${ClassName}, ${BusinessName}QueryParams } from "@/types/api/${moduleName}/${businessName}"
#end
import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName} } from "@/api/${moduleName}/${businessName}"
#if($genView)
import ${BusinessName}ViewDrawer from "./view"
#end
const { proxy } = getCurrentInstance()
#if(${dicts} != '')
#set($dictsNoSymbol=$dicts.replace("'", ""))
const { ${dictsNoSymbol} } = proxy.useDict(${dicts})
const { ${dictsNoSymbol} } = useDict(${dicts})
#end
const ${businessName}List = ref<${ClassName}[]>([])
@@ -470,13 +480,13 @@ function getList() {
})
}
// 取消按钮
/** 取消按钮 */
function cancel() {
open.value = false
reset()
}
// 表单重置
/** 表单重置 */
function reset() {
form.value = {
#foreach ($column in $columns)
@@ -511,7 +521,7 @@ function resetQuery() {
handleQuery()
}
// 多选框选中数据
/** 多选框选中数据 */
function handleSelectionChange(selection: ${ClassName}[]) {
ids.value = selection.map(item => item.${pkColumn.javaField})
single.value = selection.length != 1
@@ -615,6 +625,13 @@ function handle${subClassName}SelectionChange(selection: any[]) {
checked${subClassName}.value = selection.map(item => item.index)
}
#end
#if($genView)
/** 详情按钮操作 */
function handleViewData(row: ${ClassName}) {
proxy.#[[$]]#refs["${businessName}ViewRef"].open(row.${pkColumn.javaField})
}
#end
/** 导出按钮操作 */
function handleExport() {

View File

@@ -0,0 +1,84 @@
<template>
<el-drawer title="${functionName}详情" v-model="visible" direction="rtl" size="60%" append-to-body :before-close="handleClose" class="detail-drawer">
<div v-loading="loading" class="drawer-content">
<h4 class="section-header">基本信息</h4>
#set($i = 0)
#foreach($column in $columns)
#if(!$column.pk && $column.list)
#set($dictType=$column.dictType)
#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($i % 2 == 0)
<el-row :gutter="20" class="mb8">
#end
<el-col :span="12">
<div class="info-item">
<label class="info-label">${comment}</label>
<span class="info-value plaintext">
#if("" != $dictType)
#if($column.htmlType == "checkbox")
<dict-tag :options="${dictType}" :value="info.${javaField} ? info.${javaField}.split(',') : []" />
#else
<dict-tag :options="${dictType}" :value="info.${javaField}" />
#end
#elseif($column.htmlType == "datetime")
{{ parseTime(info.${javaField}, '{y}-{m}-{d}') }}
#elseif($column.htmlType == "imageUpload")
<image-preview :src="info.${javaField}" :width="60" :height="60" />
#else
{{ info.${javaField} }}
#end
</span>
</div>
</el-col>
#set($i = $i + 1)
#if($i % 2 == 0)
</el-row>
#end
#end
#end
#if($i % 2 != 0)
</el-row>
#end
</div>
</el-drawer>
</template>
<script setup lang="ts" name="${BusinessName}ViewDrawer">
import type { ${ClassName} } from "@/types/api/${moduleName}/${businessName}"
import { get${BusinessName} } from '@/api/${moduleName}/${businessName}'
#if(${dicts} != '')
#set($dictsNoSymbol=$dicts.replace("'", ""))
const { ${dictsNoSymbol} } = useDict(${dicts})
#end
const visible = ref<boolean>(false)
const loading = ref<boolean>(false)
const info = reactive<Partial<${ClassName}>>({})
const open = async (#if($pkColumn.javaType == "Long" || $pkColumn.javaType == "Integer")${pkColumn.javaField}: number#else${pkColumn.javaField}: string#end): Promise<void> => {
visible.value = true
loading.value = true
try {
const res = await get${BusinessName}(${pkColumn.javaField})
Object.assign(info, res.data ?? {})
} catch (error) {
console.error('获取${functionName}信息失败:', error)
} finally {
loading.value = false
}
}
const handleClose = (): void => {
visible.value = false
Object.keys(info).forEach(key => delete (info as any)[key])
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,86 @@
<template>
<el-drawer title="${functionName}详情" :visible.sync="visible" direction="rtl" size="60%" append-to-body :before-close="handleClose" custom-class="detail-drawer">
<div v-loading="loading" class="drawer-content">
<h4 class="section-header">基本信息</h4>
#set($i = 0)
#foreach($column in $columns)
#if(!$column.pk && $column.list)
#set($dictType=$column.dictType)
#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($i % 2 == 0)
<el-row :gutter="20" class="mb8">
#end
<el-col :span="12">
<div class="info-item">
<label class="info-label">${comment}</label>
<span class="info-value plaintext">
#if("" != $dictType)
#if($column.htmlType == "checkbox")
<dict-tag :options="dict.type.${dictType}" :value="info.${javaField} ? info.${javaField}.split(',') : []" />
#else
<dict-tag :options="dict.type.${dictType}" :value="info.${javaField}" />
#end
#elseif($column.htmlType == "datetime")
{{ parseTime(info.${javaField}, '{y}-{m}-{d}') }}
#else
{{ info.${javaField} }}
#end
</span>
</div>
</el-col>
#set($i = $i + 1)
#if($i % 2 == 0)
</el-row>
#end
#end
#end
#if($i % 2 != 0)
</el-row>
#end
</div>
</el-drawer>
</template>
<script>
import { get${BusinessName} } from '@/api/${moduleName}/${businessName}'
export default {
name: '${BusinessName}ViewDrawer',
#foreach($column in $columns)
#if("" != $column.dictType)
#set($hasDicts = true)
#break
#end
#end
#if($hasDicts)
dicts: [#foreach($column in $columns)#if("" != $column.dictType)'${column.dictType}'#if($foreach.hasNext), #end#end#end],
#end
data() {
return {
visible: false,
loading: false,
info: {}
}
},
methods: {
open(${pkColumn.javaField}) {
this.visible = true
this.loading = true
get${BusinessName}(${pkColumn.javaField}).then(res => {
this.info = res.data || {}
}).finally(() => {
this.loading = false
})
},
handleClose() {
this.visible = false
}
}
}
</script>

View File

@@ -54,7 +54,6 @@ public class SysNoticeController extends BaseController
/**
* 根据通知公告编号获取详细信息
*/
@RequiresPermissions("system:notice:query")
@GetMapping(value = "/{noticeId}")
public AjaxResult getInfo(@PathVariable Long noticeId)
{
@@ -125,6 +124,19 @@ public class SysNoticeController extends BaseController
return success();
}
/**
* 已读用户列表数据
*/
@RequiresPermissions("system:notice:list")
@GetMapping("/readUsers/list")
@ResponseBody
public TableDataInfo readUsersList(Long noticeId, String searchValue)
{
startPage();
List<?> list = noticeReadService.selectReadUsersByNoticeId(noticeId, searchValue);
return getDataTable(list);
}
/**
* 删除通知公告
*/

View File

@@ -189,11 +189,18 @@ public class SysUserController extends BaseController
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
ajax.put("pwdChrtype", getSysAccountChrtype());
ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
return ajax;
}
// 获取用户密码自定义配置规则
public String getSysAccountChrtype()
{
return Convert.toStr(configService.selectConfigByKey("sys.account.chrtype"), "0");
}
// 检查初始密码是否提醒修改
public boolean initPasswordIsModify(Date pwdUpdateDate)
{

View File

@@ -1,9 +1,10 @@
package com.ruoyi.system.mapper;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.system.domain.SysNoticeRead;
import com.ruoyi.system.domain.SysNotice;
import com.ruoyi.system.domain.SysNoticeRead;
/**
* 公告已读记录 数据层
@@ -55,6 +56,15 @@ public interface SysNoticeReadMapper
*/
public List<SysNotice> selectNoticeListWithReadStatus(@Param("userId") Long userId, @Param("limit") int limit);
/**
* 查询已阅读某公告的用户列表
*
* @param noticeId 公告ID
* @param searchValue 搜索值
* @return 已读用户列表
*/
public List<Map<String, Object>> selectReadUsersByNoticeId(@Param("noticeId") Long noticeId, @Param("searchValue") String searchValue);
/**
* 公告删除时清理对应已读记录
*

View File

@@ -1,6 +1,7 @@
package com.ruoyi.system.service;
import java.util.List;
import java.util.Map;
import com.ruoyi.system.domain.SysNotice;
/**
@@ -43,6 +44,15 @@ public interface ISysNoticeReadService
*/
public void markReadBatch(Long userId, Long[] noticeIds);
/**
* 查询已阅读某公告的用户列表
*
* @param noticeId 公告ID
* @param searchValue 搜索值
* @return 已读用户列表
*/
public List<Map<String, Object>> selectReadUsersByNoticeId(Long noticeId, String searchValue);
/**
* 删除公告时清理对应已读记录
*

View File

@@ -49,7 +49,7 @@ public interface ISysNoticeService
* @return 结果
*/
public int deleteNoticeById(Long noticeId);
/**
* 批量删除公告信息
*

View File

@@ -1,10 +1,11 @@
package com.ruoyi.system.service.impl;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.system.domain.SysNoticeRead;
import com.ruoyi.system.domain.SysNotice;
import com.ruoyi.system.domain.SysNoticeRead;
import com.ruoyi.system.mapper.SysNoticeReadMapper;
import com.ruoyi.system.service.ISysNoticeReadService;
@@ -62,6 +63,15 @@ public class SysNoticeReadServiceImpl implements ISysNoticeReadService
noticeReadMapper.insertNoticeReadBatch(userId, noticeIds);
}
/**
* 查询已阅读某公告的用户列表
*/
@Override
public List<Map<String, Object>> selectReadUsersByNoticeId(Long noticeId, String searchValue)
{
return noticeReadMapper.selectReadUsersByNoticeId(noticeId, searchValue);
}
/**
* 删除公告时清理对应已读记录
*/

View File

@@ -63,4 +63,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach>
</delete>
<!-- 查询已阅读某公告的用户列表,支持按登录名/用户名模糊筛选 -->
<select id="selectReadUsersByNoticeId" resultType="java.util.Map">
select
u.user_id as userId,
u.user_name as userName,
u.nick_name as nickName,
d.dept_name as deptName,
u.phonenumber as phonenumber,
r.read_time as readTime
from sys_notice_read r
inner join sys_user u on u.user_id = r.user_id and u.del_flag = '0'
left join sys_dept d on d.dept_id = u.dept_id
where r.notice_id = #{noticeId}
<if test="searchValue != null and searchValue != ''">
and (
u.user_name like concat('%', #{searchValue}, '%')
or u.nick_name like concat('%', #{searchValue}, '%')
)
</if>
order by r.read_time desc
</select>
</mapper>

View File

@@ -68,3 +68,12 @@ export function markNoticeReadAll(ids) {
params: { ids }
})
}
// 查询公告已读用户列表
export function listNoticeReadUsers(query) {
return request({
url: '/system/notice/readUsers/list',
method: 'get',
params: query
})
}

View File

@@ -469,6 +469,9 @@
}
/* 拖拽列样式 */
.allowDrag { cursor: grab; }
.allowDrag:active { cursor: grabbing; }
.sortable-ghost {
opacity: .8;
color: #fff !important;

View File

@@ -0,0 +1,362 @@
<template>
<el-drawer title="公告详情" :visible.sync="visible" direction="rtl" size="50%" append-to-body :before-close="handleClose" custom-class="notice-detail-drawer">
<div v-loading="loading" class="notice-detail-drawer__body">
<div v-if="!detail" class="notice-empty">
<i class="el-icon-document"></i>
<span>暂无数据</span>
</div>
<div v-else class="notice-page">
<div class="notice-type-wrap">
<span v-if="detail.noticeType === '1'" class="notice-type-tag type-notify">
<i class="el-icon-bell"></i> 通知
</span>
<span v-else-if="detail.noticeType === '2'" class="notice-type-tag type-announce">
<i class="el-icon-message"></i> 公告
</span>
<span v-else class="notice-type-tag type-notify">
<i class="el-icon-document"></i> 消息
</span>
</div>
<h1 class="notice-title">{{ detail.noticeTitle }}</h1>
<div class="notice-meta">
<span class="meta-item">
<i class="el-icon-user"></i>
<span>{{ detail.createBy || '—' }}</span>
</span>
<span class="meta-item">
<i class="el-icon-time"></i>
<span>{{ detail.createTime || '—' }}</span>
</span>
<span class="meta-item">
<span :class="['status-dot', isStatusNormal ? 'status-ok' : 'status-off']"></span>
<span>{{ isStatusNormal ? '正常' : '已关闭' }}</span>
</span>
</div>
<div class="notice-divider">
<span class="notice-divider-dot"></span>
<span class="notice-divider-dot"></span>
<span class="notice-divider-dot"></span>
</div>
<div class="notice-body">
<div v-if="hasContent" class="notice-content" v-html="detail.noticeContent" />
<div v-else class="notice-empty notice-empty--inner">
<i class="el-icon-document"></i> 暂无内容
</div>
</div>
</div>
</div>
</el-drawer>
</template>
<script>
import { getNotice } from '@/api/system/notice'
export default {
name: 'NoticeDetailView',
data() {
return {
visible: false,
loading: false,
detail: null
}
},
computed: {
isStatusNormal() {
const s = this.detail && this.detail.status
return s === '0' || s === 0
},
hasContent() {
const c = this.detail && this.detail.noticeContent
return c != null && String(c).trim() !== ''
}
},
methods: {
open(payload) {
let id = null
let preset = null
if (payload != null && typeof payload === 'object') {
id = payload.noticeId
if (payload.noticeContent != null) {
preset = payload
}
} else {
id = payload
}
this.visible = true
if (preset) {
this.detail = preset
return
}
if (id == null || id === '') {
this.detail = null
return
}
this.loading = true
this.detail = null
getNotice(id).then(res => {
this.detail = res.data
}).catch(() => {
this.detail = null
}).finally(() => {
this.loading = false
})
},
handleClose() {
this.visible = false
this.detail = null
this.loading = false
}
}
}
</script>
<style lang="scss" scoped>
.notice-page {
max-width: 760px;
margin: 0 auto;
padding: 8px 8px 20px;
animation: notice-fade-up 0.28s ease both;
}
@keyframes notice-fade-up {
from {
opacity: 0;
transform: translateY(14px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.notice-type-tag {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 12px;
border-radius: 2px;
font-size: 11px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
margin-bottom: 14px;
}
.type-notify {
background: #fff8e6;
color: #b7791f;
border-left: 3px solid #d97706;
}
.type-announce {
background: #e8f5e9;
color: #276749;
border-left: 3px solid #38a169;
}
.notice-title {
font-size: 22px;
font-weight: 700;
color: #1a202c;
line-height: 1.45;
margin: 0 0 16px;
letter-spacing: -0.2px;
}
.notice-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16px;
padding: 12px 0;
border-top: 1px solid #e9ecef;
border-bottom: 1px solid #e9ecef;
margin-bottom: 28px;
}
.meta-item {
display: flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: #718096;
}
.meta-item i {
font-size: 12px;
color: #a0aec0;
}
.status-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
margin-right: 4px;
}
.status-ok {
background: #38a169;
}
.status-off {
background: #e53e3e;
}
.notice-divider {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.notice-divider::before,
.notice-divider::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(to right, transparent, #dee2e6, transparent);
}
.notice-divider-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #cbd5e0;
}
.notice-body {
background: #fff;
border-radius: 6px;
padding: 28px 32px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.04);
min-height: 120px;
}
.notice-content {
font-size: 14px;
line-height: 1.85;
color: #2d3748;
word-break: break-word;
}
.notice-content ::v-deep p {
margin: 0 0 1em;
}
.notice-content ::v-deep h1,
.notice-content ::v-deep h2,
.notice-content ::v-deep h3 {
font-weight: 700;
color: #1a202c;
margin: 1.4em 0 0.6em;
}
.notice-content ::v-deep h1 {
font-size: 18px;
}
.notice-content ::v-deep h2 {
font-size: 16px;
}
.notice-content ::v-deep h3 {
font-size: 14px;
}
.notice-content ::v-deep a {
color: #3182ce;
text-decoration: underline;
}
.notice-content ::v-deep a:hover {
color: #2b6cb0;
}
.notice-content ::v-deep img {
max-width: 100%;
border-radius: 4px;
margin: 8px 0;
}
.notice-content ::v-deep ul,
.notice-content ::v-deep ol {
padding-left: 20px;
margin: 0 0 1em;
}
.notice-content ::v-deep li {
margin-bottom: 4px;
}
.notice-content ::v-deep blockquote {
border-left: 3px solid #cbd5e0;
margin: 1em 0;
padding: 6px 16px;
color: #718096;
background: #f7fafc;
}
.notice-content ::v-deep table {
border-collapse: collapse;
width: 100%;
margin: 1em 0;
font-size: 13px;
}
.notice-content ::v-deep table th,
.notice-content ::v-deep table td {
border: 1px solid #e2e8f0;
padding: 7px 12px;
}
.notice-content ::v-deep table th {
background: #f7fafc;
font-weight: 600;
}
.notice-empty {
text-align: center;
padding: 40px 0;
color: #a0aec0;
font-size: 13px;
}
.notice-empty i {
font-size: 28px;
display: block;
margin-bottom: 10px;
}
.notice-empty--inner {
padding: 32px 0;
}
.notice-empty--inner i {
font-size: 28px;
}
::v-deep .notice-detail-drawer {
.el-drawer__header {
margin-bottom: 0;
padding: 16px 20px;
border-bottom: 1px solid #ebeef5;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.el-drawer__body {
background: #f5f6f8;
}
}
.notice-detail-drawer__body {
height: 100%;
overflow: auto;
padding: 10px 16px 22px;
}
</style>

View File

@@ -23,38 +23,24 @@
<span v-if="unreadCount > 0" class="notice-badge">{{ unreadCount }}</span>
</div>
<el-dialog :title="previewTitle" :visible.sync="previewVisible" width="680px" append-to-body custom-class="notice-preview-dialog">
<div class="notice-preview-meta">
<el-tag size="small" :type="previewNoticeType === '1' ? 'warning' : 'success'">
{{ previewNoticeType === '1' ? '通知' : '公告' }}
</el-tag>
<span class="notice-preview-info"><i class="el-icon-user"></i> {{ previewCreateBy }}</span>
<span class="notice-preview-info"><i class="el-icon-time"></i> {{ previewCreateTime }}</span>
</div>
<div class="notice-preview-divider"></div>
<div class="notice-preview-content" v-html="previewContent"></div>
</el-dialog>
<notice-detail-view ref="noticeViewRef" />
</div>
</template>
<script>
import { listNoticeTop, markNoticeRead, markNoticeReadAll, getNotice } from '@/api/system/notice'
import NoticeDetailView from './DetailView'
import { listNoticeTop, markNoticeRead, markNoticeReadAll } from '@/api/system/notice'
export default {
name: 'HeaderNotice',
components: { NoticeDetailView },
data() {
return {
noticeList: [], // 通知列表
unreadCount: 0, // 未读数量
noticeLoading: false, // 加载状态
noticeVisible: false, // 弹出层显示状态
noticeLeaveTimer: null, // 鼠标离开计时器
previewVisible: false, // 预览弹窗显示状态
previewTitle: '', // 预览弹窗标题
previewContent: '', // 预览弹窗内容
previewNoticeType: '', // 预览弹窗类型
previewCreateBy: '', // 预览弹窗创建人
previewCreateTime: '' // 预览弹窗创建时间
noticeLeaveTimer: null // 鼠标离开计时器
}
},
mounted() {
@@ -99,15 +85,7 @@ export default {
if (idx !== -1) this.$set(this.noticeList, idx, { ...item, isRead: true })
this.unreadCount = Math.max(0, this.unreadCount - 1)
}
getNotice(item.noticeId).then(res => {
const notice = res.data
this.previewTitle = notice.noticeTitle
this.previewContent = notice.noticeContent
this.previewNoticeType = notice.noticeType
this.previewCreateBy = notice.createBy
this.previewCreateTime = notice.createTime
this.previewVisible = true
})
this.$refs.noticeViewRef.open(item.noticeId)
},
// 全部已读
markAllRead() {
@@ -200,30 +178,4 @@ export default {
font-size: 11px;
color: #bbb;
}
::v-deep .notice-preview-dialog {
.el-dialog__body { padding: 0 20px 20px; }
.notice-preview-meta {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 0;
font-size: 12px;
color: #888;
.notice-preview-info { display: flex; align-items: center; gap: 4px; }
}
.notice-preview-divider {
height: 1px;
background: linear-gradient(to right, transparent, #e2e8f0, transparent);
margin-bottom: 16px;
}
.notice-preview-content {
font-size: 14px;
line-height: 1.85;
color: #2d3748;
word-break: break-word;
img { max-width: 100%; border-radius: 4px; }
p { margin: 0 0 1em; }
a { color: #409EFF; text-decoration: underline; }
}
}
</style>

View File

@@ -61,7 +61,7 @@
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>开启 Tags-Views</span>
<span>开启页签</span>
<el-switch v-model="tagsView" class="drawer-switch" />
</div>
@@ -75,6 +75,14 @@
<el-switch v-model="tagsIcon" :disabled="!tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>标签页样式</span>
<el-radio-group v-model="tagsViewStyle" :disabled="!tagsView" size="mini" class="drawer-switch">
<el-radio-button label="card">卡片</el-radio-button>
<el-radio-button label="chrome">谷歌</el-radio-button>
</el-radio-group>
</div>
<div class="drawer-item">
<span>固定 Header</span>
<el-switch v-model="fixedHeader" class="drawer-switch" />
@@ -163,6 +171,17 @@ export default {
})
}
},
tagsViewStyle: {
get() {
return this.$store.state.settings.tagsViewStyle
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'tagsViewStyle',
value: val
})
}
},
sidebarLogo: {
get() {
return this.$store.state.settings.sidebarLogo
@@ -256,6 +275,7 @@ export default {
"navType":${this.navType},
"tagsView":${this.tagsView},
"tagsIcon":${this.tagsIcon},
"tagsViewStyle":"${this.tagsViewStyle}",
"tagsViewPersist":${this.tagsViewPersist},
"fixedHeader":${this.fixedHeader},
"sidebarLogo":${this.sidebarLogo},

View File

@@ -1,5 +1,5 @@
<template>
<div id="tags-view-container" class="tags-view-container">
<div id="tags-view-container" class="tags-view-container" :class="{ 'tags-view-container--chrome': tagsViewStyle === 'chrome' }" :style="chromeVars">
<!-- 左切换箭头 -->
<span class="tags-nav-btn tags-nav-btn--left" :class="{ disabled: !canScrollLeft }" @click="scrollLeft">
<i class="el-icon-arrow-left" />
@@ -15,11 +15,11 @@
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
:style="activeStyle(tag)"
:style="tagActiveStyle(tag)"
@click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent.native="openMenu(tag, $event)"
>
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" />
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" style="margin-right: 3px;" />
{{ tag.title }}
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
@@ -97,8 +97,20 @@ export default {
tagsIcon() {
return this.$store.state.settings.tagsIcon
},
tagsViewStyle() {
return this.$store.state.settings.tagsViewStyle
},
selectedDropdownTag() {
return this.visitedViews.find(v => this.isActive(v)) || {}
},
chromeVars() {
if (this.tagsViewStyle !== 'chrome') return {}
const primary = this.theme || '#409EFF'
return {
'--chrome-tab-active-bg': this.mixHexWithWhite(primary, 0.15),
'--chrome-tab-text-active': primary,
'--chrome-wing-r': '14px'
}
}
},
watch: {
@@ -136,11 +148,21 @@ export default {
this.toggleFullscreen()
}
},
mixHexWithWhite(hex, ratio) {
const clean = hex.replace('#', '')
const r = parseInt(clean.substring(0, 2), 16)
const g = parseInt(clean.substring(2, 4), 16)
const b = parseInt(clean.substring(4, 6), 16)
const mr = Math.round(r * ratio + 255 * (1 - ratio))
const mg = Math.round(g * ratio + 255 * (1 - ratio))
const mb = Math.round(b * ratio + 255 * (1 - ratio))
return `rgb(${mr}, ${mg}, ${mb})`
},
isActive(route) {
return route.path === this.$route.path
},
activeStyle(tag) {
if (!this.isActive(tag)) return {}
tagActiveStyle(tag) {
if (!this.isActive(tag) || this.tagsViewStyle !== 'card') return {}
return {
"background-color": this.theme,
"border-color": this.theme
@@ -367,13 +389,16 @@ export default {
</script>
<style lang="scss" scoped>
$tags-bar-height: 34px;
.tags-view-container {
height: 34px;
height: $tags-bar-height;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
display: flex;
align-items: center;
overflow: hidden;
$btn-width: 28px;
$btn-color: #71717a;
@@ -388,7 +413,7 @@ export default {
align-items: center;
justify-content: center;
width: $btn-width;
height: 34px;
height: $tags-bar-height;
cursor: pointer;
color: $btn-color;
font-size: 13px;
@@ -405,18 +430,14 @@ export default {
cursor: not-allowed;
}
&--left {
border-right: $divider;
}
&--right {
border-left: $divider;
}
&--left { border-right: $divider; }
&--right { border-left: $divider; }
}
.tags-view-wrapper {
flex: 1;
min-width: 0;
height: 100%;
.tags-view-item {
display: inline-block;
@@ -432,31 +453,27 @@ export default {
margin-left: 5px;
border-radius: 3px;
&:first-of-type {
margin-left: 6px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
}
}
&:first-of-type { margin-left: 6px; }
&:last-of-type { margin-right: 15px; }
}
}
&:not(.tags-view-container--chrome) .tags-view-wrapper .tags-view-item.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
}
}
.tags-view-item.active.has-icon::before {
&:not(.tags-view-container--chrome) .tags-view-wrapper .tags-view-item.active.has-icon::before {
content: none !important;
}
@@ -471,7 +488,7 @@ export default {
align-items: center;
justify-content: center;
width: $btn-width;
height: 34px;
height: $tags-bar-height;
cursor: pointer;
color: $btn-color;
font-size: 13px;
@@ -511,11 +528,174 @@ export default {
}
}
}
&.tags-view-container--chrome {
--chrome-strip-bg: #ffffff;
--chrome-strip-border: #e4e7ed;
--chrome-tab-text: #606266;
overflow: visible;
background: var(--chrome-strip-bg);
border-bottom: 1px solid var(--chrome-strip-border);
align-items: flex-end;
.tags-nav-btn {
align-self: stretch;
height: auto;
min-height: $tags-bar-height;
border-color: var(--chrome-strip-border);
}
.tags-action-btn {
border-color: var(--chrome-strip-border);
}
.tags-view-wrapper {
.tags-view-item {
display: inline-flex !important;
align-items: center;
justify-content: center;
position: relative;
z-index: 1;
height: 30px;
min-height: 30px;
margin: 0 0 -1px;
padding: 0 12px;
font-size: 13px;
font-weight: 400;
line-height: 1.2;
border: none !important;
border-radius: 0;
background: transparent !important;
color: var(--chrome-tab-text) !important;
padding-top: 0 !important;
box-shadow: none !important;
transition: background 0.12s ease, color 0.12s ease, border-radius 0.12s ease;
&::before,
&::after {
content: '' !important;
display: block !important;
position: absolute;
bottom: 0;
width: var(--chrome-wing-r);
height: var(--chrome-wing-r);
margin: 0 !important;
pointer-events: none;
background: transparent !important;
border-radius: 0 !important;
transition: box-shadow 0.12s ease;
}
&::before {
left: calc(-1 * var(--chrome-wing-r));
border-bottom-right-radius: var(--chrome-wing-r) !important;
box-shadow: none;
}
&::after {
right: calc(-1 * var(--chrome-wing-r));
border-bottom-left-radius: var(--chrome-wing-r) !important;
box-shadow: none;
}
&:first-of-type { margin-left: 6px; }
&:last-of-type { margin-right: 10px; }
&:not(.active) + .tags-view-item:not(.active) {
border-left: 1px solid #e4e7ed;
padding-left: 11px;
}
&:hover:not(.active) {
background: #f5f7fa !important;
border-radius: 6px 6px 0 0;
color: #303133 !important;
}
&.active {
height: 31px;
min-height: 31px;
padding: 0 14px;
color: var(--chrome-tab-text-active) !important;
font-weight: 500;
background: var(--chrome-tab-active-bg) !important;
border: none !important;
border-radius: var(--chrome-wing-r) var(--chrome-wing-r) 0 0;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
&::before {
box-shadow: calc(var(--chrome-wing-r) * 0.5) calc(var(--chrome-wing-r) * 0.5) 0 calc(var(--chrome-wing-r) * 0.5) var(--chrome-tab-active-bg);
}
&::after {
box-shadow: calc(var(--chrome-wing-r) * -0.5) calc(var(--chrome-wing-r) * 0.5) 0 calc(var(--chrome-wing-r) * 0.5) var(--chrome-tab-active-bg);
}
}
.el-icon-close {
margin-left: 3px;
&:before {
vertical-align: -2px;
}
}
}
}
}
}
</style>
<style lang="scss">
.tags-view-wrapper {
.el-scrollbar {
height: 100%;
overflow: hidden;
}
.el-scrollbar__wrap {
height: 34px !important;
display: flex;
align-items: center;
overflow-x: auto;
overflow-y: hidden;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
.tags-view-container:hover & {
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 3px;
transition: background-color 0.2s;
&:hover {
background-color: rgba(0, 0, 0, 0.4);
}
}
}
scrollbar-width: none;
-ms-overflow-style: none;
}
.el-scrollbar__bar {
opacity: 0;
transition: opacity 0.3s;
.tags-view-container:hover & {
opacity: 1;
}
}
.tags-view-item {
.el-icon-close {
width: 16px;
@@ -577,4 +757,4 @@ export default {
min-height: calc(100vh - 34px) !important;
overflow: auto;
}
</style>
</style>

View File

@@ -34,6 +34,11 @@ module.exports = {
*/
tagsIcon: false,
/**
* 标签页样式card 卡片默认、chrome 谷歌浏览器风格
*/
tagsViewStyle: 'card',
/**
* 是否固定头部
*/

View File

@@ -1,7 +1,7 @@
import defaultSettings from '@/settings'
import { useDynamicTitle } from '@/utils/dynamicTitle'
const { sideTheme, showSettings, navType, tagsView, tagsViewPersist, tagsIcon, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings
const { sideTheme, showSettings, navType, tagsView, tagsViewPersist, tagsIcon, tagsViewStyle, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
const state = {
@@ -13,6 +13,7 @@ const state = {
tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
tagsViewPersist: storageSetting.tagsViewPersist === undefined ? tagsViewPersist : storageSetting.tagsViewPersist,
tagsIcon: storageSetting.tagsIcon === undefined ? tagsIcon : storageSetting.tagsIcon,
tagsViewStyle: storageSetting.tagsViewStyle === undefined ? tagsViewStyle : storageSetting.tagsViewStyle,
fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle,

View File

@@ -1,5 +1,6 @@
import store from '@/store'
import router from '@/router'
import cache from '@/plugins/cache'
import { MessageBox, } from 'element-ui'
import { login, logout, getInfo, refreshToken } from '@/api/login'
import { getToken, setToken, setExpiresIn, removeToken } from '@/utils/auth'
@@ -82,6 +83,7 @@ const user = {
commit('SET_NAME', user.userName)
commit('SET_NICK_NAME', user.nickName)
commit('SET_AVATAR', avatar)
cache.session.set('pwrChrtype', res.pwdChrtype)
/* 初始密码提示 */
if(res.isDefaultModifyPwd) {
MessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {

View File

@@ -0,0 +1,71 @@
/**
* 密码强度规则
* 根据参数 chrtype 动态生成校验规则
*
* chrtype 说明:
* 0 - 任意字符(默认)
* 1 - 纯数字0-9
* 2 - 纯字母a-z / A-Z
* 3 - 字母 + 数字(必须同时包含)
* 4 - 字母 + 数字 + 特殊字符(必须同时包含,特殊字符:~!@#$%^&*()-=_+
*/
import cache from '@/plugins/cache'
// 各类型对应的正则、错误提示
const PWD_RULES = {
'0': { pattern: /^[^<>"'|\\]+$/, message: '密码不能包含非法字符:< > " \' \\ |' },
'1': { pattern: /^[0-9]+$/, message: '密码只能为数字0-9' },
'2': { pattern: /^[a-zA-Z]+$/, message: '密码只能为英文字母a-z、A-Z' },
'3': { pattern: /^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$/, message: '密码必须同时包含字母和数字' },
'4': { pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[~!@#$%^&*()\-=_+])[A-Za-z\d~!@#$%^&*()\-=_+]+$/, message: '密码必须同时包含字母、数字和特殊字符(~!@#$%^&*()-=_+' }
}
export default {
data() {
return {
// 密码限制类型
pwdChrType: cache.session.get('pwrChrtype') || '0'
}
},
computed: {
// 默认密码校验
pwdValidator() {
const rule = PWD_RULES[this.pwdChrType] || PWD_RULES['0']
return [
{ required: true, message: '密码不能为空', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度必须介于 6 和 20 之间', trigger: 'blur' },
{ pattern: rule.pattern, message: rule.message, trigger: 'blur' }
]
},
// 校验prompt的inputValidator函数
pwdPromptValidator() {
const rule = PWD_RULES['0']
return (value) => {
if (!value || value.length < 6 || value.length > 20) {
return '密码长度必须介于 6 和 20 之间'
}
if (!rule.pattern.test(value)) {
return rule.message
}
}
},
// 个人中心密码校验
infoPwdValidator() {
const rule = PWD_RULES[this.pwdChrType] || PWD_RULES['0']
return [
{ required: true, message: '新密码不能为空', trigger: 'blur' },
{ min: 6, max: 20, message: '新密码长度必须介于 6 和 20 之间', trigger: 'blur' },
{ pattern: rule.pattern, message: rule.message, trigger: 'blur' }
]
},
// 注册页面密码校验
registerPwdValidator() {
const rule = PWD_RULES['0']
return [
{ required: true, message: '请输入您的密码', trigger: 'blur' },
{ min: 6, max: 20, message: '用户密码长度必须介于 6 和 20 之间', trigger: 'blur' },
{ pattern: rule.pattern, message: rule.message, trigger: 'blur' }
]
}
}
}

View File

@@ -5,7 +5,12 @@
* @returns {Boolean}
*/
export function isPathMatch(pattern, path) {
const regexPattern = pattern.replace(/\//g, '\\/').replace(/\*\*/g, '.*').replace(/\*/g, '[^\\/]*')
const regexPattern = pattern
.replace(/([.+^${}()|\[\]\\])/g, '\\$1')
.replace(/\*\*/g, '__DOUBLE_STAR__')
.replace(/\*/g, '[^/]*')
.replace(/__DOUBLE_STAR__/g, '.*')
.replace(/\?/g, '[^/]')
const regex = new RegExp(`^${regexPattern}$`)
return regex.test(path)
}

View File

@@ -7,7 +7,7 @@
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-form-item prop="password" :rules="registerPwdValidator">
<el-input
v-model="registerForm.password"
type="password"
@@ -68,18 +68,12 @@
<script>
import { getCodeImg, register } from "@/api/login"
import passwordRule from "@/utils/passwordRule"
import defaultSettings from '@/settings'
export default {
name: "Register",
mixins: [passwordRule],
data() {
const equalToPassword = (rule, value, callback) => {
if (this.registerForm.password !== value) {
callback(new Error("两次输入的密码不一致"))
} else {
callback()
}
}
return {
title: process.env.VUE_APP_TITLE,
footerContent: defaultSettings.footerContent,
@@ -91,24 +85,31 @@ export default {
code: "",
uuid: ""
},
registerRules: {
loading: false,
captchaEnabled: true
}
},
computed: {
registerRules() {
return {
username: [
{ required: true, trigger: "blur", message: "请输入您的账号" },
{ min: 2, max: 20, message: '用户账号长度必须介于 2 和 20 之间', trigger: 'blur' }
],
password: [
{ required: true, trigger: "blur", message: "请输入您的密码" },
{ min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" },
{ pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }
],
confirmPassword: [
{ required: true, trigger: "blur", message: "请再次输入您的密码" },
{ required: true, validator: equalToPassword, trigger: "blur" }
{ required: true, message: "请再次输入您的密码", trigger: "blur" },
{
validator: (rule, value, callback) => {
if (this.registerForm.password !== value) {
callback(new Error("两次输入的密码不一致"))
} else {
callback()
}
}, trigger: "blur"
}
],
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
},
loading: false,
captchaEnabled: true
}
}
},
created() {

View File

@@ -0,0 +1,109 @@
<template>
<el-dialog :title="`「${noticeTitle}」已读用户`" :visible.sync="visible" width="760px" top="6vh" append-to-body @close="handleClose">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" style="margin-bottom: 4px;">
<el-form-item prop="searchValue">
<el-input
v-model="queryParams.searchValue"
placeholder="登录名称 / 用户名称"
clearable
prefix-icon="el-icon-search"
style="width: 220px;"
@keyup.enter.native="handleQuery"
@clear="handleQuery"
/>
</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-form-item>
<el-form-item style="float: right; margin-right: 0;">
<span class="read-stat">
<strong>{{ total }}</strong> 人已读
</span>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="userList" size="small" stripe height="340px">
<el-table-column type="index" label="序号" width="55" align="center" />
<el-table-column label="登录名称" prop="userName" align="center" :show-overflow-tooltip="true" />
<el-table-column label="用户名称" prop="nickName" align="center" :show-overflow-tooltip="true" />
<el-table-column label="所属部门" prop="deptName" align="center" :show-overflow-tooltip="true" />
<el-table-column label="手机号码" prop="phonenumber" align="center" width="120" />
<el-table-column label="阅读时间" prop="readTime" align="center" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.readTime) }}</span>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" style="padding: 6px 0px;"/>
</el-dialog>
</template>
<script>
import { listNoticeReadUsers } from "@/api/system/notice"
export default {
name: "ReadUsers",
data() {
return {
visible: false,
loading: false,
noticeId: undefined,
noticeTitle: "",
total: 0,
userList: [],
queryParams: {
pageNum: 1,
pageSize: 10,
noticeId: undefined,
searchValue: undefined
}
}
},
methods: {
open(row) {
this.noticeId = row.noticeId
this.noticeTitle = row.noticeTitle
this.queryParams.noticeId = row.noticeId
this.queryParams.searchValue = undefined
this.queryParams.pageNum = 1
this.visible = true
this.getList()
},
getList() {
this.loading = true
listNoticeReadUsers(this.queryParams).then(res => {
this.userList = res.rows
this.total = res.total
}).finally(() => {
this.loading = false
})
},
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
resetQuery() {
this.resetForm("queryForm")
this.handleQuery()
},
handleClose() {
this.userList = []
this.total = 0
this.queryParams.searchValue = undefined
}
}
}
</script>
<style scoped>
.read-stat {
font-size: 13px;
color: #606266;
line-height: 28px;
}
.read-stat strong {
color: #409eff;
font-size: 15px;
margin: 0 2px;
}
</style>

View File

@@ -72,12 +72,11 @@
<el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" prop="noticeId" width="100" />
<el-table-column
label="公告标题"
align="center"
prop="noticeTitle"
:show-overflow-tooltip="true"
/>
<el-table-column label="公告标题" align="center" :show-overflow-tooltip="true">
<template slot-scope="scope">
<a class="link-type" style="cursor:pointer" @click="handleViewData(scope.row)">{{ scope.row.noticeTitle }}</a>
</template>
</el-table-column>
<el-table-column label="公告类型" align="center" prop="noticeType" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_notice_type" :value="scope.row.noticeType"/>
@@ -96,6 +95,13 @@
</el-table-column>
<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-user"
@click="handleReadUsers(scope.row)"
v-hasPermi="['system:notice:list']"
>阅读用户</el-button>
<el-button
size="mini"
type="text"
@@ -166,14 +172,20 @@
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<notice-detail-view ref="noticeViewRef" />
<read-users-dialog ref="readUsersRef" />
</div>
</template>
<script>
import NoticeDetailView from "@/layout/components/HeaderNotice/DetailView"
import ReadUsersDialog from "./ReadUsers"
import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice"
export default {
name: "Notice",
components: { NoticeDetailView, ReadUsersDialog },
dicts: ['sys_notice_status', 'sys_notice_type'],
data() {
return {
@@ -297,6 +309,14 @@ export default {
}
})
},
/** 查看公告详情 */
handleViewData(row) {
this.$refs.noticeViewRef.open(row)
},
/** 查看已读用户 */
handleReadUsers(row) {
this.$refs.readUsersRef.open(row)
},
/** 删除按钮操作 */
handleDelete(row) {
const noticeIds = row.noticeId || this.ids

View File

@@ -116,7 +116,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password" :rules="pwdValidator">
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
</el-form-item>
</el-col>
@@ -181,9 +181,11 @@ import "@riophae/vue-treeselect/dist/vue-treeselect.css"
import TreePanel from "@/components/TreePanel"
import ExcelImportDialog from "@/components/ExcelImportDialog"
import UserViewDrawer from "./view"
import passwordRule from "@/utils/passwordRule"
export default {
name: "User",
mixins: [passwordRule],
dicts: ['sys_normal_disable', 'sys_user_sex'],
components: { Treeselect, TreePanel, ExcelImportDialog, UserViewDrawer },
data() {
@@ -248,11 +250,6 @@ export default {
nickName: [
{ required: true, message: "用户昵称不能为空", trigger: "blur" }
],
password: [
{ required: true, message: "用户密码不能为空", trigger: "blur" },
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' },
{ pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }
],
email: [
{
type: "email",
@@ -405,17 +402,11 @@ export default {
},
/** 重置密码按钮操作 */
handleResetPwd(row) {
this.$prompt('请输入"' + row.userName + '"的新密码', "提示", {
this.$prompt(`请输入「${row.userName}的新密码`, "重置密码", {
confirmButtonText: "确定",
cancelButtonText: "取消",
closeOnClickModal: false,
inputPattern: /^.{5,20}$/,
inputErrorMessage: "用户密码长度必须介于 5 和 20 之间",
inputValidator: (value) => {
if (/<|>|"|'|\||\\/.test(value)) {
return "不能包含非法字符:< > \" ' \\\ |"
}
},
inputValidator: this.pwdPromptValidator
}).then(({ value }) => {
resetUserPwd(row.userId, value).then(() => {
this.$modal.msgSuccess("修改成功,新密码是:" + value)

View File

@@ -1,9 +1,9 @@
<template>
<el-form ref="form" :model="user" :rules="rules" label-width="80px">
<el-form ref="form" :model="user" :rules="formRules" label-width="80px">
<el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password/>
</el-form-item>
<el-form-item label="新密码" prop="newPassword">
<el-form-item label="新密码" prop="newPassword" :rules="infoPwdValidator">
<el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
@@ -18,35 +18,36 @@
<script>
import { updateUserPwd } from "@/api/system/user"
import passwordRule from "@/utils/passwordRule"
export default {
mixins: [passwordRule],
data() {
const equalToPassword = (rule, value, callback) => {
if (this.user.newPassword !== value) {
callback(new Error("两次输入的密码不一致"))
} else {
callback()
}
}
return {
user: {
oldPassword: undefined,
newPassword: undefined,
confirmPassword: undefined
},
// 表单校验
rules: {
}
}
},
computed: {
formRules() {
return {
oldPassword: [
{ required: true, message: "旧密码不能为空", trigger: "blur" }
],
newPassword: [
{ required: true, message: "新密码不能为空", trigger: "blur" },
{ min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" },
{ pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }
],
confirmPassword: [
{ required: true, message: "确认密码不能为空", trigger: "blur" },
{ required: true, validator: equalToPassword, trigger: "blur" }
{
validator: (rule, value, callback) => {
if (this.user.newPassword !== value) {
callback(new Error("两次输入的密码不一致"))
} else {
callback()
}
}, trigger: "blur"
}
]
}
}

View File

@@ -183,6 +183,7 @@ export default {
const genTable = Object.assign({}, basicForm.model, genForm.model)
genTable.columns = this.columns
genTable.params = {
genView: genTable.view ? '1' : '0',
treeCode: genTable.treeCode,
treeName: genTable.treeName,
treeParentCode: genTable.treeParentCode,

View File

@@ -69,7 +69,7 @@
</el-form-item>
</el-col>
<el-col :span="24">
<el-col :span="12">
<el-form-item prop="formColNum">
<span slot="label">
表单布局
@@ -85,6 +85,13 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="genView">
<span slot="label">扩展功能</span>
<el-checkbox v-model="info.view">生成详情页</el-checkbox>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="genType">
<span slot="label">

View File

@@ -552,6 +552,7 @@ insert into sys_config values(4, '账号自助-是否开启用户注册功能',
insert into sys_config values(5, '用户登录-黑名单列表', 'sys.login.blackIPList', '', 'Y', 'admin', sysdate(), '', null, '设置登录IP黑名单限制多个匹配项以;分隔,支持匹配(*通配、网段)');
insert into sys_config values(6, '用户管理-初始密码修改策略', 'sys.account.initPasswordModify', '1', 'Y', 'admin', sysdate(), '', null, '0初始密码修改策略关闭没有任何提示1提醒用户如果未修改初始密码则在登录时就会提醒修改密码对话框');
insert into sys_config values(7, '用户管理-账号密码更新周期', 'sys.account.passwordValidateDays', '0', 'Y', 'admin', sysdate(), '', null, '密码更新周期填写数字数据初始化值为0不限制若修改必须为大于0小于365的正整数如果超过这个周期登录系统时则在登录时就会提醒修改密码对话框');
insert into sys_config values(8, '用户管理-密码字符范围', 'sys.account.chrtype', '0', 'Y', 'admin', sysdate(), '', null, '默认任意字符范围0任意密码可以输入任意字符1数字密码只能为0-9数字2英文字母密码只能为a-z和A-Z字母3字母和数字密码必须包含字母数字,4字母数字和特殊字符目前支持的特殊字符包括~!@#$%^&*()-=_+');
-- ----------------------------