fix: 更新路由为后台配置,修改admim文件名称为admin

This commit is contained in:
2025-03-12 09:05:28 +08:00
parent 6735d8dedb
commit ace23e89ee
29 changed files with 2107 additions and 617 deletions

View File

@@ -0,0 +1,537 @@
<template>
<div class="app-container">
<div class="search-bar">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item label="关键字" prop="title">
<el-input
v-model="queryParams.title"
placeholder="菜单名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="never">
<div class="mb-10px">
<el-button
v-hasPerm="['sys:menu:add']"
type="success"
icon="plus"
@click="handleOpenDialog('0')"
>
新增
</el-button>
</div>
<el-table
v-loading="loading"
:data="menuTableData"
highlight-current-row
row-key="menuId"
:tree-props="{
children: 'children',
hasChildren: 'hasChildren',
}"
@row-click="handleRowClick"
>
<el-table-column label="菜单名称" min-width="100">
<template #default="scope">
{{ scope.row.title }}
</template>
</el-table-column>
<el-table-column label="图标" align="center" width="80">
<template #default="scope">
<template v-if="scope.row.icon && scope.row.icon.startsWith('el-icon')">
<el-icon style="vertical-align: -0.15em">
<component :is="scope.row.icon.replace('el-icon-', '')" />
</el-icon>
</template>
<template v-else-if="scope.row.icon">
<svg-icon :icon-class="scope.row.icon" />
</template>
</template>
</el-table-column>
<el-table-column label="类型" align="center" width="80">
<template #default="scope">
<el-tag
v-if="scope.row.type === MenuTypeEnum.MENU && scope.row.path.startsWith('/')"
type="warning"
>
目录
</el-tag>
<el-tag
v-if="scope.row.type === MenuTypeEnum.MENU && !scope.row.path.startsWith('/')"
type="success"
>
菜单
</el-tag>
<el-tag v-if="scope.row.type === MenuTypeEnum.BUTTON" type="danger">按钮</el-tag>
<el-tag v-if="scope.row.type === MenuTypeEnum.EXTLINK" type="info">外链</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" align="left" width="150" prop="menuSort" />
<el-table-column label="路由路径" align="left" width="250" prop="path" />
<el-table-column label="组件路径" align="left" width="250" prop="component" />
<el-table-column label="外链" align="left" width="80" prop="iFrame">
<template #default="scope">
{{ scope.row.iFrame ? "是" : "否" }}
</template>
</el-table-column>
<el-table-column label="缓存" align="left" width="80" prop="cache">
<template #default="scope">
{{ scope.row.cache ? "是" : "否" }}
</template>
</el-table-column>
<el-table-column label="可见" align="left" width="80" prop="iFrame">
<template #default="scope">
{{ scope.row.hidden ? "否" : "是" }}
</template>
</el-table-column>
<el-table-column label="创建日期" align="left" prop="createTime">
<template #default="scope">
{{ scope.row.createTime }}
</template>
</el-table-column>
<el-table-column fixed="right" align="center" label="操作" width="220">
<template #default="scope">
<el-button
v-if="scope.row.type == 0"
v-hasPerm="['sys:menu:add']"
type="primary"
link
size="small"
icon="plus"
@click.stop="handleOpenDialog(scope.row.menuId)"
>
新增
</el-button>
<el-button
v-hasPerm="['sys:menu:edit']"
type="primary"
link
size="small"
icon="edit"
@click.stop="handleOpenDialog(undefined, scope.row.menuId)"
>
编辑
</el-button>
<el-button
v-hasPerm="['sys:menu:delete']"
type="danger"
link
size="small"
icon="delete"
@click.stop="handleDelete(scope.row.menuId)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-drawer v-model="dialog.visible" :title="dialog.title" size="50%" @close="handleCloseDialog">
<el-form ref="editRequestRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="父级菜单" prop="pid">
<el-tree-select
v-model="formData.pid"
placeholder="选择上级菜单"
:data="menuOptions"
filterable
check-strictly
:render-after-expand="false"
/>
</el-form-item>
<el-form-item label="菜单名称" prop="title">
<el-input v-model="formData.title" placeholder="请输入菜单名称" />
</el-form-item>
<el-form-item label="菜单类型" prop="type">
<el-radio-group v-model="formData.type" @change="handleMenuTypeChange">
<el-radio :value="0">菜单</el-radio>
<el-radio :value="1">按钮</el-radio>
<el-radio :value="2">接口</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="是否外链" prop="path">
<el-switch
v-model="formData.iFrame"
:active-value="1"
:inactive-value="0"
active-text=""
inactive-text=""
/>
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="name">
<template #label>
<div class="flex-y-center">
路由名称
<el-tooltip placement="bottom" effect="light">
<template #content>
如果需要开启缓存需保证页面 defineOptions 中的 name 与此处一致建议使用驼峰
</template>
<el-icon class="ml-1 cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input v-model="formData.name" placeholder="User" />
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="path">
<template #label>
<div class="flex-y-center">
路由路径
<el-tooltip placement="bottom" effect="light">
<template #content>
定义应用中不同页面对应的 URL 路径目录需以 / 开头菜单项不用例如系统管理目录
/system系统管理下的用户管理菜单 user
</template>
<el-icon class="ml-1 cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input
v-if="formData.type == MenuTypeEnum.MENU"
v-model="formData.path"
placeholder="system"
/>
<el-input v-else v-model="formData.path" placeholder="user" />
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="component">
<template #label>
<div class="flex-y-center">
组件路径
<el-tooltip placement="bottom" effect="light">
<template #content>
组件页面完整路径相对于 src/views/ system/user/index缺省后缀 .vue
</template>
<el-icon class="ml-1 cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input v-model="formData.component" placeholder="system/user/index" style="width: 95%">
<template v-if="formData.type == MenuTypeEnum.MENU" #prepend>src/views/</template>
<template v-if="formData.type == MenuTypeEnum.MENU" #append>.vue</template>
</el-input>
</el-form-item>
<el-form-item v-if="formData.type !== MenuTypeEnum.BUTTON" prop="hidden" label="显示状态">
<el-radio-group v-model="formData.hidden">
<el-radio :value="1">隐藏</el-radio>
<el-radio :value="0">显示</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.type === MenuTypeEnum.MENU" label="缓存页面">
<el-radio-group v-model="formData.cache">
<el-radio :value="true">开启</el-radio>
<el-radio :value="false">关闭</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序" prop="menuSort">
<el-input-number
v-model="formData.menuSort"
style="width: 100px"
controls-position="right"
:min="0"
/>
</el-form-item>
<!-- 权限标识 -->
<el-form-item
v-if="formData.type == MenuTypeEnum.BUTTON"
label="权限标识"
prop="permission"
>
<el-input v-model="formData.permission" placeholder="sys:user:add" />
</el-form-item>
<el-form-item v-if="formData.type !== MenuTypeEnum.BUTTON" label="图标" prop="icon">
<!-- 图标选择器 -->
<icon-select v-model="formData.icon" />
</el-form-item>
<!-- <el-form-item v-if="formData.type == MenuTypeEnum.MENU" label="跳转路由">
<el-input v-model="formData.redirect" placeholder="跳转路由" />
</el-form-item> -->
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit"> </el-button>
<el-button @click="handleCloseDialog"> </el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "SysMenu",
inheritAttrs: false,
});
import MenuAPI, { getListRequest, MenuVO, editRequest } from "@/api/account/menu";
import { MenuTypeEnum } from "@/enums/MenuTypeEnum";
const queryFormRef = ref();
const editRequestRef = ref();
const loading = ref(false);
const dialog = reactive({
title: "新增菜单",
visible: false,
});
// 查询参数
const queryParams = reactive<getListRequest>({});
// 菜单表格数据
const menuTableData = ref<MenuVO[]>([]);
// 顶级菜单下拉选项
const menuOptions = ref<OptionType[]>([]);
// 初始菜单表单数据
const initialeditRequestData = ref<editRequest>({
id: "",
pid: "0",
hidden: 0,
menuSort: 1,
type: 0, // 默认菜单
alwaysShow: false,
cache: 0,
title: "",
icon: "",
iFrame: 0,
permission: "",
path: "",
component: "",
name: "",
});
// 菜单表单数据
const formData = ref({ ...initialeditRequestData.value });
// 表单验证规则
const rules = reactive({
pid: [{ required: false, message: "请选择父级菜单", trigger: "blur" }],
title: [{ required: true, message: "请输入菜单名称", trigger: "blur" }],
type: [{ required: true, message: "请选择菜单类型", trigger: "blur" }],
name: [{ required: false, message: "请输入路由名称", trigger: "blur" }],
path: [{ required: true, message: "请输入路由路径", trigger: "blur" }],
component: [{ required: false, message: "请输入组件路径", trigger: "blur" }],
hidden: [{ required: true, message: "请选择显示状态", trigger: "change" }],
});
// 选择表格的行菜单ID
const selectedMenuId = ref<string | undefined>();
// 查询菜单
function handleQuery() {
loading.value = true;
MenuAPI.getList(queryParams)
.then((data) => {
menuTableData.value = data;
})
.finally(() => {
loading.value = false;
});
}
// 重置查询
function handleResetQuery() {
queryFormRef.value.resetFields();
handleQuery();
}
// 行点击事件
function handleRowClick(row: MenuVO) {
selectedMenuId.value = row.id;
}
/**
* 返回格式化后的menus菜单数据
*/
function returnFilterMenuData(menus: MenuVO[]): OptionType[] {
return menus.map((menu) => {
const { title, menuId, children, ...rest } = menu;
return {
value: menuId,
label: title,
children: children && returnFilterMenuData(children),
...rest,
} as OptionType;
});
}
/**
* 打开表单弹窗
*
* @param pid 父菜单ID
* @param menuId 菜单ID
*/
function handleOpenDialog(pid?: string, menuId?: string) {
MenuAPI.getList({})
.then((data) => {
menuOptions.value = [{ value: "0", label: "顶级菜单", children: returnFilterMenuData(data) }];
})
.then(() => {
dialog.visible = true;
if (menuId) {
console.log(menuId);
dialog.title = "编辑菜单";
MenuAPI.get(menuId).then((data) => {
console.log(data);
data.iFrame;
formData.value = {
...data,
id: data.id || "",
pid: data.pid || "0",
cache: data.cache ? 1 : 0,
hidden: data.hidden ? 1 : 0,
component: data.component || "",
icon: data.icon || "",
iFrame: data.iFrame ? 1 : 0,
menuSort: data.menuSort ?? 0, // Ensure menuSort is always a number
name: data.name || "", // Ensure name is always a string
path: data.path || "", // Ensure path is always a string
title: data.title || "", // Ensure title is always a string
type: data.type ?? 0, // Ensure type is always a number
};
});
} else {
dialog.title = "新增菜单";
formData.value.pid = pid ? pid : "0";
}
});
}
// 菜单类型切换
function handleMenuTypeChange() {
// 如果菜单类型改变
if (formData.value.type !== initialeditRequestData.value.type) {
if (formData.value.type === MenuTypeEnum.MENU) {
// 目录切换到菜单时,清空组件路径
if (initialeditRequestData.value.type === MenuTypeEnum.MENU) {
formData.value.component = "";
} else {
// 其他情况,保留原有的组件路径
formData.value.path = initialeditRequestData.value.path;
formData.value.component = initialeditRequestData.value.component;
}
}
}
}
/**
* 提交表单
*/
function handleSubmit() {
editRequestRef.value.validate((isValid: boolean) => {
if (isValid) {
const menuId = formData.value.menuId;
const submitFormData = {
...formData.value,
cache: formData.value.cache ? 1 : 0,
hidden: formData.value.hidden ? 1 : 0,
pid: formData.value.pid == 0 ? undefined : String(formData.value.pid),
};
if (menuId) {
//修改时父级菜单不能为当前菜单
if (formData.value.pid == menuId) {
ElMessage.error("父级菜单不能为当前菜单");
return;
}
MenuAPI.edit(menuId, submitFormData).then(() => {
ElMessage.success("修改成功");
handleCloseDialog();
handleQuery();
});
} else {
MenuAPI.add(submitFormData).then(() => {
ElMessage.success("新增成功");
handleCloseDialog();
handleQuery();
});
}
}
});
}
// 删除菜单
function handleDelete(menuId: number) {
if (!menuId) {
ElMessage.warning("请勾选删除项");
return false;
}
ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(
() => {
loading.value = true;
MenuAPI.delete(menuId)
.then(() => {
ElMessage.success("删除成功");
handleQuery();
})
.finally(() => {
loading.value = false;
});
},
() => {
ElMessage.info("已取消删除");
}
);
}
function resetForm() {
editRequestRef.value.resetFields();
editRequestRef.value.clearValidate();
formData.value = {
id: "",
pid: "0",
hidden: 0,
menuSort: 1,
type: 0, // 默认菜单
alwaysShow: false,
cache: 0,
title: "",
icon: "",
iFrame: 0,
permission: "",
path: "",
component: "",
name: "",
};
}
// 关闭弹窗
function handleCloseDialog() {
dialog.visible = false;
resetForm();
}
onMounted(() => {
handleQuery();
});
</script>

View File

@@ -0,0 +1,91 @@
import API, { type addRequest } from "@/api/system/miniAppPages";
import type { IModalConfig } from "@/components/CURD/types";
const modalConfig: IModalConfig<addRequest> = {
pageName: "sys:user",
dialog: {
title: "新增小程序页面",
width: 800,
draggable: true,
},
form: {
labelWidth: 140,
},
formAction: function (data) {
if (data.icon) {
data.icon = typeof data.icon === "string" ? data.icon : data.icon[0]
}
return API.add(data);
},
beforeSubmit(data) {
console.log("提交之前处理", data);
},
formItems: [
{
type: "UpImage",
label: "图标",
prop: "icon",
rules: [{ required: false, message: "请上传图标", trigger: "blur" }],
attrs: {
placeholder: "请上传图标",
},
},
{
type: "input",
label: "小程序页面名称",
prop: "name",
rules: [{ required: true, message: "请输入小程序页面名称", trigger: "blur" }],
attrs: {
placeholder: "请输入小程序页面名称",
},
},
{
type: "input",
label: "小程序页面路径",
prop: "path",
rules: [{ required: true, message: "请输入小程序页面路径", trigger: "blur" }],
attrs: {
placeholder: "请输入小程序页面路径",
},
},
{
type: "textarea",
label: "页面描述",
prop: "description",
rules: [{ required: false, message: "请输入页面描述", trigger: "blur" }],
attrs: {
placeholder: "请输入页面描述",
},
},
{
type: "input-number",
label: "排序",
prop: "sort",
rules: [{ required: false, message: "请输入排序", trigger: "blur" }],
attrs: {
placeholder: "请输入排序",
},
initialValue: 0,
},
{
type: "radio-button",
label: "小程序页面状态",
prop: "status",
options: [
{
label: "启用",
value: 1,
},
{
label: "禁用",
value: 0,
},
],
initialValue: 1,
}
],
};
// 如果有异步数据会修改配置的推荐用reactive包裹而纯静态配置的可以直接导出
export default reactive(modalConfig);

View File

@@ -0,0 +1,31 @@
import { property } from "lodash";
export const statusOptions: options[] = [
{ label: "全部", value: '' },
{ label: "启用", value: 1 },
{ label: "禁用", value: 0 },
];
export type optionsType = "status";
export function returnOptions(type: optionsType) {
if (type === "status") {
return statusOptions;
}
}
export function returnOptionsLabel(optionsType: optionsType, value: string | number) {
const options = returnOptions(optionsType);
if (!options) {
return "";
}
const option = options.find((item) => item.value === value);
return option ? option.label : "";
}
export interface options {
label: string;
value: string | number;
[property: string]: any;
}

View File

@@ -0,0 +1,73 @@
import API, { type editRequest } from "@/api/system/miniAppPages";
import type { IContentConfig } from "@/components/CURD/types";
const contentConfig: IContentConfig<editRequest> = {
pageName: "sys:user",
table: {
border: true,
highlightCurrentRow: true,
},
pagination: {
background: true,
layout: "prev,pager,next,jumper,total,sizes",
pageSize: 20,
pageSizes: [10, 20, 30, 50],
},
indexAction: function (params) {
return API.getList(params);
},
deleteAction: API.delete,
// modifyAction: function (data) {
// // return API.edit(data);
// },
pk: "id",
toolbar: ["add"],
defaultToolbar: ["refresh", "filter", "search"],
cols: [
{ type: "selection", width: 50, align: "center" },
{
label: "小程序页面名称",
align: "center",
prop: "name",
},
{
label: "图标",
align: "center",
prop: "icon",
templet: "custom",
slotName: "icon",
},
{
label: "小程序页面路径",
align: "center",
prop: "path",
},
{
label: "页面描述",
align: "center",
prop: "description",
},
{
label: "排序",
align: "center",
prop: "sort",
},
{
label: "状态",
align: "center",
prop: "status",
templet: "custom",
slotName: "status",
},
{
label: "操作",
align: "center",
fixed: "right",
width: 280,
templet: "tool",
operat: ["edit", "delete"],
},
],
};
export default contentConfig;

View File

@@ -0,0 +1,91 @@
import API, { type editRequest } from "@/api/system/miniAppPages";
import type { IModalConfig } from "@/components/CURD/types";
const modalConfig: IModalConfig<editRequest> = {
pageName: "sys:user",
dialog: {
title: "编辑小程序页面",
width: 800,
draggable: true,
},
form: {
labelWidth: 140,
},
formAction: function (data) {
if (data.icon) {
data.icon = typeof data.icon === "string" ? data.icon : data.icon[0]
}
return API.edit(data);
},
beforeSubmit(data) {
console.log("提交之前处理", data);
},
formItems: [
{
type: "UpImage",
label: "图标",
prop: "icon",
rules: [{ required: false, message: "请上传图标", trigger: "blur" }],
attrs: {
placeholder: "请上传图标",
},
},
{
type: "input",
label: "小程序页面名称",
prop: "name",
rules: [{ required: true, message: "请输入小程序页面名称", trigger: "blur" }],
attrs: {
placeholder: "请输入小程序页面名称",
},
},
{
type: "input",
label: "小程序页面路径",
prop: "path",
rules: [{ required: true, message: "请输入小程序页面路径", trigger: "blur" }],
attrs: {
placeholder: "请输入小程序页面路径",
},
},
{
type: "textarea",
label: "页面描述",
prop: "description",
rules: [{ required: false, message: "请输入页面描述", trigger: "blur" }],
attrs: {
placeholder: "请输入页面描述",
},
},
{
type: "input-number",
label: "排序",
prop: "sort",
rules: [{ required: false, message: "请输入排序", trigger: "blur" }],
attrs: {
placeholder: "请输入排序",
},
initialValue: 0,
},
{
type: "radio-button",
label: "小程序页面状态",
prop: "status",
options: [
{
label: "启用",
value: 1,
},
{
label: "禁用",
value: 0,
},
],
initialValue: 1,
}
],
};
// 如果有异步数据会修改配置的推荐用reactive包裹而纯静态配置的可以直接导出
export default reactive(modalConfig);

View File

@@ -0,0 +1,40 @@
import type { ISearchConfig } from "@/components/CURD/types";
import { statusOptions } from './config'
const searchConfig: ISearchConfig = {
pageName: "sys:user",
formItems: [
{
type: "input",
label: "小程序页面路径",
prop: "name",
attrs: {
placeholder: "请输入小程序页面路径",
clearable: true,
style: {
width: "200px",
},
},
},
{
type: "input",
label: "小程序页面路径",
prop: "path",
attrs: {
placeholder: "请输入小程序页面路径",
clearable: true,
style: {
width: "200px",
},
},
},
{
type: "radio-button",
label: "小程序页面状态",
prop: "status",
options: statusOptions,
initialValue: '',
}
],
};
export default searchConfig;

View File

@@ -0,0 +1,124 @@
<template>
<div class="app-container">
<!-- 列表 -->
<!-- 搜索 -->
<page-search
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
/>
<!-- 列表 -->
<page-content
ref="contentRef"
:content-config="contentConfig"
@add-click="handleAddClick"
@edit-click="handleEditClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operat-click="handleOperatClick"
@filter-change="handleFilterChange"
>
<template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
</el-tag>
</template>
<template #icon="scope">
<el-image
style="width: 50px; height: 50px"
:src="scope.row[scope.prop]"
:preview-src-list="[scope.row[scope.prop]]"
/>
</template>
<template #options="scope">
{{ returnOptionsLabel(scope.prop, scope.row[scope.prop]) }}
</template>
<template #gender="scope">
<DictLabel v-model="scope.row[scope.prop]" code="gender" />
</template>
<template #mobile="scope">
<el-text>{{ scope.row[scope.prop] }}</el-text>
<copy-button
v-if="scope.row[scope.prop]"
:text="scope.row[scope.prop]"
style="margin-left: 2px"
/>
</template>
</page-content>
<!-- 新增 -->
<page-modal ref="addModalRef" :modal-config="addModalConfig" @submit-click="handleSubmitClick">
<template #url="scope">
<FileUpload v-model="scope.formData[scope.prop]" :limit="1" v-bind="scope.attrs" />
<!-- <Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" /> -->
</template>
</page-modal>
<!-- 编辑 -->
<page-modal
ref="editModalRef"
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
>
<template #url="scope">
<FileUpload v-model="scope.formData[scope.prop]" :limit="1" v-bind="scope.attrs" />
<!-- <Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" /> -->
</template>
</page-modal>
</div>
</template>
<script setup lang="ts">
// import Api from "@/api/system/miniProgramVersion";
import type { IObject, IOperatData } from "@/components/CURD/types";
import usePage from "@/components/CURD/usePage";
import addModalConfig from "./config/add";
import contentConfig from "./config/content";
import editModalConfig from "./config/edit";
import searchConfig from "./config/search";
import { returnOptionsLabel } from "./config/config";
const {
searchRef,
contentRef,
addModalRef,
editModalRef,
handleQueryClick,
handleResetClick,
// handleAddClick,
// handleEditClick,
handleSubmitClick,
handleExportClick,
handleSearchClick,
handleFilterChange,
} = usePage();
// 新增
async function handleAddClick() {
addModalRef.value?.setModalVisible();
// addModalConfig.formItems[2]!.attrs!.data =
}
// 编辑
async function handleEditClick(row: IObject) {
editModalRef.value?.handleDisabled(false);
editModalRef.value?.setModalVisible();
// 根据id获取数据进行填充
// const data = await Api.getFormData(row.id);
console.log({ ...row, icon: row.icon ? [row.icon] : [] });
editModalRef.value?.setFormData({ ...row, icon: row.icon ? [row.icon] : [] });
}
1;
// 其他工具栏
function handleToolbarClick(name: string) {
console.log(name);
if (name === "custom1") {
ElMessage.success("点击了自定义1按钮");
}
}
// 其他操作列
async function handleOperatClick(data: IOperatData) {
console.log(data);
}
</script>

View File

@@ -0,0 +1,66 @@
import ParamsApi, { type addRequest } from "@/api/system/params";
import type { IModalConfig } from "@/components/CURD/types";
import { paramTypeOptions } from './config'
const modalConfig: IModalConfig<addRequest> = {
pageName: "sys:user",
dialog: {
title: "添加系统参数",
width: 800,
draggable: true,
},
form: {
labelWidth: 140,
},
formAction: function (data) {
return ParamsApi.add(data);
},
beforeSubmit(data) {
console.log("提交之前处理", data);
},
formItems: [
{
label: "参数编码",
prop: "paramCode",
rules: [{ required: true, message: "请输入参数编码", trigger: "blur" }],
type: "input",
attrs: {
placeholder: "请输入参数编码",
},
},
{
label: "参数值",
prop: "paramValue",
rules: [{ required: true, message: "请输入参数值", trigger: "blur" }],
type: "input",
attrs: {
placeholder: "请输入参数值",
},
},
{
label: "参数类型",
prop: "paramType",
rules: [{ required: true, message: "请选择参数类型", trigger: "blur" }],
type: "select",
attrs: {
placeholder: "请选择参数类型",
},
col: {
xs: 24,
sm: 12,
},
options: paramTypeOptions,
},
{
type: "textarea",
label: "备注",
prop: "remark",
rules: [{ required: true, message: "请输入备注", trigger: "blur" }],
attrs: {
placeholder: "请输入备注",
},
},
],
};
// 如果有异步数据会修改配置的推荐用reactive包裹而纯静态配置的可以直接导出
export default reactive(modalConfig);

View File

@@ -0,0 +1,28 @@
export const paramTypeOptions: options[] = [
{ label: "系统参数", value: 0 },
{ label: "非系统参数", value: 1 },
];
export type optionsType = "paramType" | "";
export function returnOptions(type: optionsType) {
if (type === "paramType") {
return paramTypeOptions;
}
}
export function returnOptionsLabel(optionsType: optionsType, value: string | number) {
const options = returnOptions(optionsType);
if (!options) {
return "";
}
const option = options.find((item) => item.value === value);
return option ? option.label : "";
}
export interface options {
label: string;
value: string | number;
[property: string]: any;
}

View File

@@ -0,0 +1,61 @@
import ParamsApi from "@/api/system/params";
import type { editRequest } from "@/api/system/params";
import type { IContentConfig } from "@/components/CURD/types";
const contentConfig: IContentConfig<editRequest> = {
pageName: "sys:user",
table: {
border: true,
highlightCurrentRow: true,
},
pagination: {
background: true,
layout: "prev,pager,next,jumper,total,sizes",
pageSize: 20,
pageSizes: [10, 20, 30, 50],
},
indexAction: function (params) {
return ParamsApi.getList(params);
},
// modifyAction: function (data) {
// // return ParamsApi.edit(data);
// },
pk: "id",
toolbar: ["add"],
defaultToolbar: ["refresh", "filter", "search"],
cols: [
{ type: "selection", width: 50, align: "center" },
{
label: "参数编码",
align: "center",
prop: "paramCode",
},
{
label: "参数值",
align: "center",
prop: "paramValue",
},
{
label: "参数类型",
align: "center",
prop: "paramType",
templet: "custom",
slotName: "options",
},
{
label: "备注",
align: "center",
prop: "remark",
},
{
label: "操作",
align: "center",
fixed: "right",
templet: "tool",
operat: ["edit"],
},
],
};
export default contentConfig;

View File

@@ -0,0 +1,133 @@
import type { IContentConfig } from "@/components/CURD/types";
const contentConfig: IContentConfig = {
pageName: "sys:user",
table: {
showOverflowTooltip: true,
},
toolbar: [],
indexAction: function (params) {
// 模拟发起网络请求获取列表数据
console.log("indexAction:", params);
return Promise.resolve({
total: 2,
list: [
{
id: 1,
username: "tom",
avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
percent: 99,
price: 10,
url: "https://www.baidu.com",
icon: "el-icon-setting",
gender: 1,
status: 1,
status2: 1,
sort: 99,
createTime: 1715647982437,
},
{
id: 2,
username: "jerry",
avatar: "https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif",
percent: 88,
price: 999,
url: "https://www.google.com",
icon: "el-icon-user",
gender: 0,
status: 0,
status2: 0,
sort: 0,
createTime: 1715648977426,
},
],
});
},
modifyAction(data) {
// 模拟发起网络请求修改字段
// console.log("modifyAction:", data);
ElMessage.success(JSON.stringify(data));
return Promise.resolve(null);
},
cols: [
{ type: "index", width: 50, align: "center" },
{ label: "ID", align: "center", prop: "id", show: false },
{ label: "文本", align: "center", prop: "username" },
{ label: "图片", align: "center", prop: "avatar", templet: "image" },
{
label: "百分比",
align: "center",
prop: "percent",
templet: "percent",
},
{
label: "货币符",
align: "center",
prop: "price",
templet: "price",
priceFormat: "$",
},
{ label: "链接", align: "center", prop: "url", width: 180, templet: "url" },
{ label: "图标", align: "center", prop: "icon", templet: "icon" },
{
label: "列表值",
align: "center",
prop: "gender",
templet: "list",
selectList: { "0": "女", "1": "男" },
},
{
label: "自定义",
align: "center",
prop: "status",
templet: "custom",
slotName: "status",
},
{
label: "Switch",
align: "center",
prop: "status2",
templet: "switch",
activeValue: 1,
inactiveValue: 0,
activeText: "启用",
inactiveText: "禁用",
},
{
label: "输入框",
align: "center",
prop: "sort",
templet: "input",
inputType: "number",
},
{
label: "日期格式化",
align: "center",
prop: "createTime",
minWidth: 120,
templet: "date",
dateFormat: "YYYY/MM/DD HH:mm:ss",
},
{
label: "操作栏",
align: "center",
fixed: "right",
width: 220,
templet: "tool",
operat: [
{
name: "reset_pwd",
auth: "password:reset",
icon: "refresh-left",
text: "重置密码",
type: "primary",
render(row) {
return row.id === 1;
},
},
],
},
],
};
export default contentConfig;

View File

@@ -0,0 +1,67 @@
import ParamsApi, { type addRequest } from "@/api/system/params";
import type { IModalConfig } from "@/components/CURD/types";
import { paramTypeOptions } from './config'
const modalConfig: IModalConfig<addRequest> = {
pageName: "sys:user",
dialog: {
title: "编辑系统参数",
width: 800,
draggable: true,
},
form: {
labelWidth: 140,
},
formAction: function (data) {
return ParamsApi.edit(data);
},
beforeSubmit(data) {
console.log("提交之前处理", data);
},
formItems: [
{
label: "参数编码",
prop: "paramCode",
rules: [{ required: true, message: "请输入参数编码", trigger: "blur" }],
type: "input",
disabled: true,
attrs: {
placeholder: "请输入参数编码",
},
},
{
label: "参数值",
prop: "paramValue",
rules: [{ required: true, message: "请输入参数值", trigger: "blur" }],
type: "input",
attrs: {
placeholder: "请输入参数值",
},
},
{
label: "参数类型",
prop: "paramType",
rules: [{ required: true, message: "请选择参数类型", trigger: "blur" }],
type: "select",
attrs: {
placeholder: "请选择参数类型",
},
col: {
xs: 24,
sm: 12,
},
options: paramTypeOptions,
},
{
type: "textarea",
label: "备注",
prop: "remark",
rules: [{ required: true, message: "请输入备注", trigger: "blur" }],
attrs: {
placeholder: "请输入备注",
},
},
],
};
// 如果有异步数据会修改配置的推荐用reactive包裹而纯静态配置的可以直接导出
export default reactive(modalConfig);

View File

@@ -0,0 +1,24 @@
import type { ISearchConfig } from "@/components/CURD/types";
import { paramTypeOptions } from './config'
const searchConfig: ISearchConfig = {
pageName: "sys:user",
formItems: [
{
type: "radio-button",
label: "参数类型",
prop: "type",
attrs: {
placeholder: "请选择参数类型",
clearable: true,
style: {
width: "200px",
},
},
options: paramTypeOptions,
initialValue: paramTypeOptions[0].value
},
],
};
export default searchConfig;

View File

@@ -0,0 +1,143 @@
<template>
<div class="app-container">
<!-- 列表 -->
<template v-if="isA">
<!-- 搜索 -->
<page-search
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
/>
<!-- 列表 -->
<page-content
ref="contentRef"
:content-config="contentConfig"
@add-click="handleAddClick"
@edit-click="handleEditClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operat-click="handleOperatClick"
@filter-change="handleFilterChange"
>
<template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
</el-tag>
</template>
<template #options="scope">
{{ returnOptionsLabel(scope.prop, scope.row[scope.prop]) }}
</template>
<template #gender="scope">
<DictLabel v-model="scope.row[scope.prop]" code="gender" />
</template>
<template #mobile="scope">
<el-text>{{ scope.row[scope.prop] }}</el-text>
<copy-button
v-if="scope.row[scope.prop]"
:text="scope.row[scope.prop]"
style="margin-left: 2px"
/>
</template>
</page-content>
<!-- 新增 -->
<page-modal
ref="addModalRef"
:modal-config="addModalConfig"
@submit-click="handleSubmitClick"
>
<template #url="scope">
<FileUpload v-model="scope.formData[scope.prop]" :limit="1" v-bind="scope.attrs" />
<!-- <Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" /> -->
</template>
</page-modal>
<!-- 编辑 -->
<page-modal
ref="editModalRef"
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
>
<template #url="scope">
<FileUpload v-model="scope.formData[scope.prop]" :limit="1" v-bind="scope.attrs" />
<!-- <Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" /> -->
</template>
</page-modal>
</template>
<template v-else>
<page-content
ref="contentRef"
:content-config="contentConfig2"
@operat-click="handleOperatClick"
>
<template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</page-content>
</template>
</div>
</template>
<script setup lang="ts">
import type { IObject, IOperatData } from "@/components/CURD/types";
import usePage from "@/components/CURD/usePage";
import addModalConfig from "./config/add";
import contentConfig from "./config/content";
import contentConfig2 from "./config/content2";
import editModalConfig from "./config/edit";
import searchConfig from "./config/search";
import { returnOptionsLabel } from "./config/config";
const {
searchRef,
contentRef,
addModalRef,
editModalRef,
handleQueryClick,
handleResetClick,
// handleAddClick,
// handleEditClick,
handleSubmitClick,
handleExportClick,
handleSearchClick,
handleFilterChange,
} = usePage();
// 新增
async function handleAddClick() {
addModalRef.value?.setModalVisible();
}
// 编辑
async function handleEditClick(row: IObject) {
editModalRef.value?.handleDisabled(false);
editModalRef.value?.setModalVisible();
// 根据id获取数据进行填充
editModalRef.value?.setFormData({ ...row });
}
1;
// 其他工具栏
function handleToolbarClick(name: string) {
console.log(name);
if (name === "custom1") {
ElMessage.success("点击了自定义1按钮");
}
}
// 其他操作列
async function handleOperatClick(data: IOperatData) {}
// 切换示例
const isA = ref(true);
</script>
<style lang="scss" scoped>
:deep(.el-table td.el-table__cell div) {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
</style>

View File

@@ -0,0 +1,134 @@
<template>
<div class="flex-x-between">
<el-input v-model="permKeywords" clearable class="w-[150px]" placeholder="菜单菜单名称">
<template #prefix>
<Search />
</template>
</el-input>
<div class="flex-center ml-5">
<el-button type="primary" size="small" plain @click="togglePermTree">
<template #icon>
<Switch />
</template>
{{ isExpanded ? "收缩" : "展开" }}
</el-button>
<el-checkbox v-model="parentChildLinked" class="ml-5" @change="handleparentChildLinkedChange">
父子联动
</el-checkbox>
<el-tooltip placement="bottom">
<template #content>
如果只需勾选菜单菜单不需要勾选子菜单或者按钮菜单请关闭父子联动
</template>
<el-icon class="ml-1 color-[--el-color-primary] inline-block cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<el-tree
ref="permTreeRef"
node-key="value"
show-checkbox
:data="menuPermOptions"
:filter-node-method="handlePermFilter"
:default-expand-all="true"
:check-strictly="!parentChildLinked"
class="mt-5"
>
<template #default="{ data }">
{{ data.label }}
</template>
</el-tree>
</template>
<script setup>
import { useUserStore } from "@/store/modules/user";
const shopUser = useUserStore();
import MenuAPI from "@/api/account/menu";
const modelValue = defineModel("modelValue", {
type: Array,
required: true,
default: () => [],
});
const permKeywords = ref("");
const isExpanded = ref(true);
const parentChildLinked = ref(true);
const permTreeRef = ref();
// 展开/收缩 菜单菜单树
function togglePermTree() {
isExpanded.value = !isExpanded.value;
if (permTreeRef.value) {
Object.values(permTreeRef.value.store.nodesMap).forEach((node) => {
if (isExpanded.value) {
node.expand();
} else {
node.collapse();
}
});
}
}
function handlePermFilter(value, data) {
if (!value) return true;
return data.label.includes(value);
}
// 父子菜单节点是否联动
function handleparentChildLinkedChange(val) {
parentChildLinked.value = val;
}
// 菜单菜单下拉
const menuPermOptions = ref([]);
function returnMenu(menu) {
return menu.map((v) => {
return {
...v,
label: v.title,
value: v.menuId,
children: v.children ? returnMenu(v.children) : [],
};
});
}
// 获取所有的菜单
async function getMenuPermOptions() {
console.log(shopUser.userInfo);
let arr =
shopUser.userInfo.account === "admin" ? await MenuAPI.getList() : await MenuAPI.getRoutes();
menuPermOptions.value = returnMenu(arr);
}
getMenuPermOptions();
onMounted(() => {
console.log(modelValue.value);
});
watch(
() => modelValue.value,
(newval) => {}
);
function getPerms() {
return permTreeRef.value.getCheckedKeys();
}
function reset() {
console.log("reset");
permTreeRef.value.setCheckedKeys([]);
}
function setChecked(checkedMenuIds) {
checkedMenuIds.forEach((menuId) => {
console.log(menuId);
permTreeRef.value.setChecked(menuId, true, false);
});
}
defineExpose({
getPerms,
setChecked,
reset,
});
</script>

View File

@@ -0,0 +1,495 @@
<template>
<div class="app-container">
<div class="search-bar">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item prop="keywords" label="关键字">
<el-input
v-model="queryParams.key"
placeholder="角色名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="never">
<div class="mb-10px">
<el-button type="primary" icon="plus" @click="handleOpenDialog()">新增</el-button>
<el-button type="danger" :disabled="ids.length === 0" icon="delete" @click="handleDelete()">
删除
</el-button>
</div>
<el-table
ref="dataTableRef"
v-loading="loading"
:data="roleList"
highlight-current-row
:border="true"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="角色名称" prop="name" />
<el-table-column label="角色级别" prop="level" />
<el-table-column label="描述" prop="description" />
<el-table-column label="创建日期" prop="createTime" />
<el-table-column fixed="right" label="操作" width="220">
<template #default="scope">
<el-button
type="primary"
size="small"
link
icon="position"
@click="handleOpenAssignPermDialog(scope.row)"
>
分配菜单
</el-button>
<el-button
type="primary"
size="small"
link
icon="edit"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
link
icon="delete"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.page"
v-model:limit="queryParams.size"
@pagination="handleQuery"
/>
</el-card>
<!-- 角色表单弹窗 -->
<el-dialog
v-model="dialog.visible"
:title="dialog.title"
width="500px"
@close="handleCloseDialog"
>
<el-form ref="addRequestRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="角色名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入角色名称" />
</el-form-item>
<el-form-item label="角色等级" prop="level">
<el-input-number placeholder="角色等级" v-model="formData.level"></el-input-number>
</el-form-item>
<!-- <el-form-item label="数据菜单" prop="dataScope">
<el-select v-model="formData.dataScope">
<el-option :key="0" label="全部数据" :value="0" />
<el-option :key="1" label="部门及子部门数据" :value="1" />
<el-option :key="2" label="本部门数据" :value="2" />
<el-option :key="3" label="本人数据" :value="3" />
</el-select>
</el-form-item> -->
<el-form-item label="描述" prop="description">
<el-input type="textarea" v-model="formData.description" placeholder="描述" />
</el-form-item>
<el-form-item label="菜单分配" prop="menuIdList">
<menuSelect ref="refmenuSelect" v-model="formData.menuIdList"></menuSelect>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit"> </el-button>
<el-button @click="handleCloseDialog"> </el-button>
</div>
</template>
</el-dialog>
<!-- 分配菜单弹窗 -->
<el-drawer
v-model="assignPermDialogVisible"
:title="'【' + checkedRole.name + '】菜单分配'"
size="500"
>
<div class="flex-x-between">
<el-input v-model="permKeywords" clearable class="w-[150px]" placeholder="菜单菜单名称">
<template #prefix>
<Search />
</template>
</el-input>
<div class="flex-center ml-5">
<el-button type="primary" size="small" plain @click="togglePermTree">
<template #icon>
<Switch />
</template>
{{ isExpanded ? "收缩" : "展开" }}
</el-button>
<el-checkbox
v-model="parentChildLinked"
class="ml-5"
@change="handleparentChildLinkedChange"
>
父子联动
</el-checkbox>
<el-tooltip placement="bottom">
<template #content>
如果只需勾选菜单菜单不需要勾选子菜单或者按钮菜单请关闭父子联动
</template>
<el-icon class="ml-1 color-[--el-color-primary] inline-block cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<el-tree
ref="permTreeRef"
node-key="value"
show-checkbox
:data="menuPermOptions"
:filter-node-method="handlePermFilter"
:default-expand-all="true"
:check-strictly="!parentChildLinked"
class="mt-5"
>
<template #default="{ data }">
{{ data.label }}
</template>
</el-tree>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleAssignPermSubmit"> </el-button>
<el-button @click="assignPermDialogVisible = false"> </el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "Role",
inheritAttrs: false,
});
import { useUserStore } from "@/store/modules/user";
const shopUser = useUserStore();
import menuSelect from "./components/menus.vue";
import RoleApi, { SysRole, addRequest, getListRequest } from "@/api/account/role";
import MenuAPI, { type RouteVO } from "@/api/account/menu";
const queryFormRef = ref();
const addRequestRef = ref();
const refmenuSelect = ref();
const permTreeRef = ref();
const loading = ref(false);
const ids = ref<number[]>([]);
const total = ref(0);
const queryParams = reactive<getListRequest>({
page: 1,
size: 10,
key: "",
});
// 角色表格数据
const roleList = ref<SysRole[]>();
// 菜单菜单下拉
const menuPermOptions = ref<RouteVO[]>([]);
// 弹窗
const dialog = reactive({
title: "",
visible: false,
});
// 角色表单
const formData = reactive<addRequest>({
// sort: 1,
// status: 1,
id: null,
name: "",
level: 0,
menuIdList: [],
description: "",
});
const rules = reactive({
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
level: [{ required: false, message: "请输入角色等级", trigger: "blur" }],
});
// 选中的角色
interface CheckedRole {
id?: number;
name?: string;
}
const checkedRole = ref<CheckedRole>({});
const assignPermDialogVisible = ref(false);
const permKeywords = ref("");
const isExpanded = ref(true);
const parentChildLinked = ref(true);
// 查询
function handleQuery() {
loading.value = true;
RoleApi.getList(queryParams)
.then((data) => {
roleList.value = data.records;
total.value = data.totalRow;
})
.finally(() => {
loading.value = false;
});
}
// 重置查询
function handleResetQuery() {
queryFormRef.value.resetFields();
queryParams.page = 1;
queryParams.size = 10;
queryParams.key = "";
handleQuery();
}
// 行复选框选中
function handleSelectionChange(selection: any) {
ids.value = selection.map((item: any) => item.id);
}
// 打开角色弹窗
async function handleOpenDialog(row: SysRole) {
dialog.visible = true;
if (row && row.id) {
dialog.title = "修改角色";
//获取角色菜单列表
const data = await RoleApi.getMenu(row.id);
console.log(data);
Object.assign(formData, row);
formData.menuIdList = data;
setTimeout(() => {
refmenuSelect.value.setChecked(data);
}, 300);
console.log(formData);
} else {
dialog.title = "新增角色";
}
}
// 提交角色表单
function handleSubmit() {
addRequestRef.value.validate((valid: any) => {
if (valid) {
const checkedMenuIds: number[] = refmenuSelect.value.getPerms();
loading.value = true;
const roleId = formData.id;
if (roleId) {
RoleApi.update(roleId, { ...formData, menuIdList: checkedMenuIds })
.then(() => {
ElMessage.success("修改成功");
handleCloseDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
} else {
delete formData.id;
RoleApi.add({ ...formData, menuIdList: checkedMenuIds })
.then(() => {
ElMessage.success("新增成功");
handleCloseDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
}
}
});
}
// 关闭弹窗
function handleCloseDialog() {
addRequestRef.value.resetFields();
addRequestRef.value.clearValidate();
refmenuSelect.value.reset();
formData.id = undefined;
formData.sort = 1;
formData.status = 1;
dialog.visible = false;
}
// 删除角色
function handleDelete(roleId?: number) {
const roleIds = [roleId || ids.value].join(",");
if (!roleIds) {
ElMessage.warning("请勾选删除项");
return;
}
ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(
() => {
loading.value = true;
RoleApi.delete({ id: roleId ?? null })
.then(() => {
ElMessage.success("删除成功");
handleResetQuery();
})
.finally(() => (loading.value = false));
},
() => {
ElMessage.info("已取消删除");
}
);
}
interface typeRoute extends RouteVO {
label: string | number;
value: string | number;
children: typeRoute[];
}
function returnMenu(menu) {
return menu.map((v) => {
return {
...v,
label: v.title,
value: v.menuId,
children: v.children ? returnMenu(v.children) : [],
};
});
}
// 获取所有的菜单
async function getMenuPermOptions() {
let arr =
shopUser.userInfo.account === "admin" ? await MenuAPI.getList() : await MenuAPI.getRoutes();
menuPermOptions.value = returnMenu(arr);
}
getMenuPermOptions();
// 打开分配菜单菜单弹窗
async function handleOpenAssignPermDialog(row: SysRole) {
const roleId = row.id;
if (roleId) {
assignPermDialogVisible.value = true;
loading.value = true;
checkedRole.value.id = roleId;
checkedRole.value.name = row.name as string;
// 回显角色已拥有的菜单
RoleApi.getMenu(roleId)
.then((data) => {
const checkedMenuIds = data;
checkedMenuIds.forEach((menuId) => permTreeRef.value!.setChecked(menuId, true, false));
})
.finally(() => {
loading.value = false;
});
}
}
// 分配菜单菜单提交
function handleAssignPermSubmit() {
const roleId = checkedRole.value.id;
const name = checkedRole.value.name;
if (roleId) {
const checkedMenuIds: number[] = permTreeRef
.value!.getCheckedNodes(false, true)
.map((node: any) => node.value);
loading.value = true;
RoleApi.update(roleId, { name, menuIdList: checkedMenuIds })
.then(() => {
ElMessage.success("分配菜单成功");
assignPermDialogVisible.value = false;
handleResetQuery();
})
.finally(() => {
loading.value = false;
});
}
}
// 展开/收缩 菜单菜单树
function togglePermTree() {
isExpanded.value = !isExpanded.value;
if (permTreeRef.value) {
Object.values(permTreeRef.value.store.nodesMap).forEach((node: any) => {
if (isExpanded.value) {
node.expand();
} else {
node.collapse();
}
});
}
}
function editTogglePermTree() {
isExpanded.value = !isExpanded.value;
if (permTreeRef.value) {
Object.values(permTreeRef.value.store.nodesMap).forEach((node: any) => {
if (isExpanded.value) {
node.expand();
} else {
node.collapse();
}
});
}
}
// 菜单筛选
watch(permKeywords, (val) => {
permTreeRef.value!.filter(val);
});
watch(
() => assignPermDialogVisible.value,
(val) => {
if (!val) {
permTreeRef.value.setCheckedKeys([]);
}
}
);
function handlePermFilter(
value: string,
data: {
[key: string]: any;
}
) {
if (!value) return true;
return data.label.includes(value);
}
// 父子菜单节点是否联动
function handleparentChildLinkedChange(val: any) {
parentChildLinked.value = val;
}
onMounted(() => {
handleQuery();
});
</script>

View File

@@ -0,0 +1 @@
<template></template>

View File

@@ -0,0 +1,296 @@
<!-- 文件上传组件 -->
<template>
<div>
<el-upload
v-model:file-list="fileList"
:auto-upload="true"
:style="props.style"
:before-upload="handleBeforeUpload"
:http-request="handleUpload"
:on-progress="handleProgress"
:on-success="handleSuccess"
:on-error="handleError"
:accept="props.accept"
:limit="props.limit"
multiple
>
<!-- 上传文件按钮 -->
<el-button type="primary" :disabled="fileList.length >= props.limit">
{{ props.uploadBtnText }}
</el-button>
<!-- 文件列表 -->
<template #file="{ file }">
<div class="el-upload-list__item-info">
<a class="el-upload-list__item-name" @click="handleDownload(file)">
<el-icon><Document /></el-icon>
<span class="el-upload-list__item-file-name">{{ file.name }}</span>
<span class="el-icon--close" @click="handleRemove(file.url!)">
<el-icon><Close /></el-icon>
</span>
</a>
</div>
</template>
</el-upload>
<el-progress
:style="{
display: showProgress ? 'inline-flex' : 'none',
width: '100%',
}"
:percentage="progressPercent"
/>
</div>
</template>
<script lang="ts" setup>
import {
UploadRawFile,
UploadUserFile,
UploadProgressEvent,
UploadRequestOptions,
} from "element-plus";
import VersionApi from "@/api/system/version";
import OSS from "@/utils/oss-upload.js";
let ossClient: any = null;
async function getCredentials() {
const res = await VersionApi.getCredentials();
ossClient = new OSS({ ...res, stsToken: res.securityToken });
console.log(ossClient);
}
onMounted(() => {
getCredentials();
});
import CommonApi, { FileInfo, uploadResponse } from "@/api/account/common";
const props = defineProps({
version: {
type: [Boolean, Number],
default: "",
},
/**
* 请求携带的额外参数
*/
data: {
type: Object,
default: () => {
return {};
},
},
/**
* 上传文件的参数名
*/
name: {
type: String,
default: "file",
},
/**
* 文件上传数量限制
*/
limit: {
type: Number,
default: 1,
},
/**
* 单个文件上传大小限制(单位MB)
*/
maxFileSize: {
type: Number,
default: 300,
},
/**
* 上传文件类型
*/
accept: {
type: String,
default: "*",
},
/**
* 上传按钮文本
*/
uploadBtnText: {
type: String,
default: "上传文件",
},
/**
* 样式
*/
style: {
type: Object,
default: () => {
return {
width: "300px",
};
},
},
});
const modelValue = defineModel("modelValue", {
type: [Array] as PropType<string[]>,
required: true,
default: () => [],
});
const fileList = ref([] as UploadUserFile[]);
const showProgress = ref(false);
const progressPercent = ref(0);
// 监听 modelValue 转换用于显示的 fileList
watch(
modelValue,
(value) => {
fileList.value = value.map((url) => {
const name = url.substring(url.lastIndexOf("/") + 1);
return {
name: name,
url: url,
} as UploadUserFile;
});
},
{
immediate: true,
}
);
/**
* 上传前校验
*/
function handleBeforeUpload(file: UploadRawFile) {
// 限制文件大小
if (file.size > props.maxFileSize * 1024 * 1024) {
ElMessage.warning("上传图片不能大于" + props.maxFileSize + "M");
return false;
}
return true;
}
/*
* 上传文件
*/
function handleUpload(options: UploadRequestOptions) {
return new Promise((resolve, reject) => {
const file = options.file;
const formData = new FormData();
formData.append(props.name, file);
// 处理附加参数
Object.keys(props.data).forEach((key) => {
formData.append(key, props.data[key]);
});
const name = file.name;
showProgress.value = true;
ossClient
.partUpload(`/version/${props.version}${name.replace(".apk", "")}`, file, (p: number) => {
console.log("进度", p);
handleProgress({
percent: Math.floor(p * 100),
lengthComputable: true,
loaded: p * file.size,
total: file.size,
target: null,
timeStamp: Date.now(),
type: "",
eventPhase: 0,
bubbles: false,
cancelable: false,
defaultPrevented: false,
isTrusted: true,
returnValue: true,
srcElement: null,
currentTarget: null,
composed: false,
cancelBubble: false,
NONE: 0,
CAPTURING_PHASE: 1,
AT_TARGET: 2,
BUBBLING_PHASE: 3,
composedPath: () => [],
initEvent: () => {},
preventDefault: () => {},
stopImmediatePropagation: () => {},
stopPropagation: () => {},
});
// 这里可以根据进度做相应的处理例如更新UI等
})
.then((data: any) => {
console.log(data);
resolve(data);
})
.catch((error: any) => {
reject(error);
});
// CommonApi.upload(formData)
// .then((data) => {
// resolve(data);
// })
// .catch((error) => {
// reject(error);
// });
});
}
/**
* 上传进度
*
* @param event
*/
const handleProgress = (event: UploadProgressEvent) => {
progressPercent.value = event.percent;
if (progressPercent.value >= 100) {
showProgress.value = false;
}
};
/**
* 上传成功
*/
const handleSuccess = (fileInfo: string) => {
ElMessage.success("上传成功");
modelValue.value = [...modelValue.value, fileInfo];
};
const handleError = (error: any) => {
ElMessage.error("上传失败");
};
/**
* 删除文件
*/
function handleRemove(fileUrl: string) {
// CommonApi.delete(fileUrl).then(() => {
modelValue.value = modelValue.value.filter((url) => url !== fileUrl);
// });
}
/**
* 下载文件
*/
function handleDownload(file: UploadUserFile) {
// const { url, name } = file;
// if (url) {
// CommonApi.download(url, name);
// }
}
</script>
<style lang="scss" scoped>
.el-upload-list__item .el-icon--close {
position: absolute;
top: 50%;
right: 5px;
color: var(--el-text-color-regular);
cursor: pointer;
opacity: 0.75;
transition: opacity var(--el-transition-duration);
transform: translateY(-50%);
}
:deep(.el-upload-list) {
margin: 0;
}
:deep(.el-upload-list__item) {
margin: 0;
}
</style>

View File

@@ -0,0 +1,91 @@
import VersionApi, { type addRequest } from "@/api/system/version";
import { sourceOptions, typeOptions, isForceOptions } from "./config";
import type { IModalConfig } from "@/components/CURD/types";
const modalConfig: IModalConfig<addRequest> = {
pageName: "sys:user",
dialog: {
title: "新增版本",
width: 800,
draggable: true,
},
form: {
labelWidth: 140,
},
formAction: function (data) {
return VersionApi.add({ ...data, url: typeof data.url === "string" ? data.url : data.url[0] });
},
beforeSubmit(data) {
console.log("提交之前处理", data);
},
formItems: [
{
label: "渠道",
prop: "source",
rules: [{ required: true, message: "请选择渠道", trigger: "blur" }],
type: "select",
attrs: {
placeholder: "请选择渠道",
},
options: sourceOptions,
},
{
label: "类型",
prop: "type",
rules: [{ required: true, message: "请选择类型", trigger: "blur" }],
type: "select",
attrs: {
placeholder: "请选择类型",
},
col: {
xs: 24,
sm: 12,
},
options: typeOptions,
},
{
type: "input",
label: "版本号",
prop: "version",
rules: [{ required: true, message: "请输入版本号", trigger: "blur" }],
attrs: {
placeholder: "请输入版本号",
},
watch: () => { }
},
{
type: "radio",
label: "是否强制更新",
prop: "isForce",
rules: [{ required: true, message: "请输入版本号", trigger: "blur" }],
attrs: {
placeholder: "请输入版本号",
},
initialValue: 0,
options: isForceOptions,
},
{
type: "textarea",
label: "更新提示内容",
prop: "message",
rules: [{ required: true, message: "请输入更新提示内容", trigger: "blur" }],
attrs: {
placeholder: "请输入更新提示内容",
},
},
{
type: "custom",
label: "版本文件",
prop: "url",
rules: [{ required: true, message: "请上传版本文件", trigger: "blur" }],
attrs: {
placeholder: "请上传版本文件",
},
hidden: true,
initialValue: [],
},
],
};
// 如果有异步数据会修改配置的推荐用reactive包裹而纯静态配置的可以直接导出
export default reactive(modalConfig);

View File

@@ -0,0 +1,45 @@
import { property } from "lodash";
export const sourceOptions: options[] = [
{ label: "桌面端", value: "pc" },
{ label: "管理端", value: "manager_app" },
{ label: "电话机点餐", value: "phone_book " },
];
export const typeOptions: options[] = [
{ label: "windows", value: "0" },
{ label: "安卓", value: "1" },
{ label: "IOS", value: "2" },
];
export const isForceOptions: options[] = [
{ label: "不强制更新", value: 0 },
{ label: "强制更新", value: 1 },
];
export type optionsType = "source" | "type" | "isForce";
export function returnOptions(type: optionsType) {
if (type === "source") {
return sourceOptions;
}
if (type === "type") {
return typeOptions;
}
if (type === "isForce") {
return isForceOptions;
}
}
export function returnOptionsLabel(optionsType: optionsType, value: string | number) {
const options = returnOptions(optionsType);
if (!options) {
return "";
}
const option = options.find((item) => item.value === value);
return option ? option.label : "";
}
export interface options {
label: string;
value: string | number;
[property: string]: any;
}

View File

@@ -0,0 +1,88 @@
import VersionApi from "@/api/system/version";
import type { editRequest } from "@/api/system/version";
import type { IContentConfig } from "@/components/CURD/types";
const contentConfig: IContentConfig<editRequest> = {
pageName: "sys:user",
table: {
border: true,
highlightCurrentRow: true,
},
pagination: {
background: true,
layout: "prev,pager,next,jumper,total,sizes",
pageSize: 20,
pageSizes: [10, 20, 30, 50],
},
indexAction: function (params) {
return VersionApi.getList();
},
modifyAction: function (data: any) {
return VersionApi.edit(data);
},
deleteAction: VersionApi.delete,
// modifyAction: function (data) {
// // return VersionApi.edit(data);
// },
pk: "id",
toolbar: ["add"],
defaultToolbar: ["refresh", "filter", "search"],
cols: [
{ type: "selection", width: 50, align: "center" },
{ label: "id", align: "center", prop: "id", width: 100, show: true },
{
label: "渠道",
align: "center",
prop: "source",
width: 120,
templet: "custom",
slotName: "options",
},
{
label: "类型",
align: "center",
prop: "type",
width: 120,
templet: "custom",
slotName: "options",
},
{
label: "版本号",
align: "center",
width: 120,
prop: "version",
},
{
label: "是否强制升级",
align: "center",
prop: "isForce",
width: 120,
templet: "switch",
slotName: "isForce",
},
{
label: "更新内容",
align: "center",
prop: "message",
width: 240,
},
{
label: "下载地址",
align: "center",
prop: "url",
templet: "url",
slotName: "url",
},
{
label: "操作",
align: "center",
fixed: "right",
width: 280,
templet: "tool",
operat: ["edit", "delete"],
},
],
};
export default contentConfig;

View File

@@ -0,0 +1,86 @@
import VersionApi, { type editRequest } from "@/api/system/version";
import type { IModalConfig } from "@/components/CURD/types";
import { sourceOptions, typeOptions, isForceOptions } from "./config";
const modalConfig: IModalConfig<editRequest> = {
pageName: "sys:user",
dialog: {
title: "编辑版本",
width: 800,
draggable: true,
},
pk: "id",
formAction: function (data) {
return VersionApi.edit({ ...data, url: typeof data.url === "string" ? data.url : data.url[0] });
},
beforeSubmit(data) {
console.log("提交之前处理", data);
},
formItems: [
{
label: "渠道",
prop: "source",
rules: [{ required: true, message: "请选择渠道", trigger: "blur" }],
type: "select",
attrs: {
placeholder: "请选择渠道",
},
options: sourceOptions,
},
{
label: "类型",
prop: "type",
rules: [{ required: true, message: "请选择类型", trigger: "blur" }],
type: "select",
attrs: {
placeholder: "请选择类型",
},
col: {
xs: 24,
sm: 12,
},
options: typeOptions,
},
{
type: "input",
label: "版本号",
prop: "version",
rules: [{ required: true, message: "请输入版本号", trigger: "blur" }],
attrs: {
placeholder: "请输入版本号",
},
},
{
type: "radio",
label: "是否强制更新",
prop: "isForce",
rules: [{ required: true, message: "请输入版本号", trigger: "blur" }],
attrs: {
placeholder: "请输入版本号",
},
initialValue: 0,
options: isForceOptions,
},
{
type: "textarea",
label: "更新提示内容",
prop: "message",
rules: [{ required: true, message: "请输入更新提示内容", trigger: "blur" }],
attrs: {
placeholder: "请输入更新提示内容",
},
},
{
type: "custom",
label: "版本文件",
prop: "url",
rules: [{ required: true, message: "请上传版本文件", trigger: "blur" }],
attrs: {
placeholder: "请上传版本文件",
},
initialValue: [],
},
],
};
export default reactive(modalConfig);

View File

@@ -0,0 +1,21 @@
import type { ISearchConfig } from "@/components/CURD/types";
const searchConfig: ISearchConfig = {
pageName: "sys:user",
formItems: [
{
type: "input",
label: "版本号",
prop: "keywords",
attrs: {
placeholder: "请输入版本号",
clearable: true,
style: {
width: "200px",
},
},
},
],
};
export default searchConfig;

View File

@@ -0,0 +1,142 @@
<template>
<div class="app-container">
<!-- 列表 -->
<!-- 搜索 -->
<page-search
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
/>
<!-- 列表 -->
<page-content
ref="contentRef"
:content-config="contentConfig"
@add-click="handleAddClick"
@edit-click="handleEditClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operat-click="handleOperatClick"
@filter-change="handleFilterChange"
>
<template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
</el-tag>
</template>
<template #options="scope">
{{ returnOptionsLabel(scope.prop, scope.row[scope.prop]) }}
</template>
<template #gender="scope">
<DictLabel v-model="scope.row[scope.prop]" code="gender" />
</template>
<template #mobile="scope">
<el-text>{{ scope.row[scope.prop] }}</el-text>
<copy-button
v-if="scope.row[scope.prop]"
:text="scope.row[scope.prop]"
style="margin-left: 2px"
/>
</template>
</page-content>
<!-- 新增 -->
<page-modal
ref="addModalRef"
@form-data-change="handleFormDataChange"
:modal-config="addModalConfig"
@submit-click="handleSubmitClick"
>
<template #url="scope">
<version-file
:version="version"
v-model="scope.formData[scope.prop]"
v-bind="scope.attrs"
></version-file>
</template>
</page-modal>
<!-- 编辑 -->
<page-modal
ref="editModalRef"
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
>
<template #url="scope">
<version-file
:version="version"
v-model="scope.formData[scope.prop]"
v-bind="scope.attrs"
></version-file>
</template>
</page-modal>
</div>
</template>
<script setup lang="ts">
import versionFile from "./components/version-file.vue";
import VersionApi from "@/api/system/version";
import type { IObject, IOperatData } from "@/components/CURD/types";
import usePage from "@/components/CURD/usePage";
import addModalConfig from "./config/add";
import contentConfig from "./config/content";
import editModalConfig from "./config/edit";
import searchConfig from "./config/search";
import { returnOptionsLabel } from "./config/config";
let version = ref<string | number>("");
function handleFormDataChange(type: string, value: string | number) {
version.value = value;
if (type === "version" && value !== "") {
addModalConfig.formItems[5].hidden = false;
return;
}
if (type === "version" && value == "") {
addModalConfig.formItems[5].hidden = true;
}
}
const refVersionFile = ref<any>();
const {
searchRef,
contentRef,
addModalRef,
editModalRef,
handleQueryClick,
handleResetClick,
// handleAddClick,
// handleEditClick,
handleSubmitClick,
handleExportClick,
handleSearchClick,
handleFilterChange,
} = usePage();
// 新增
async function handleAddClick() {
addModalRef.value?.setModalVisible();
// addModalConfig.formItems[2]!.attrs!.data =
}
// 编辑
async function handleEditClick(row: IObject) {
editModalRef.value?.handleDisabled(false);
editModalRef.value?.setModalVisible();
// 根据id获取数据进行填充
// const data = await VersionApi.getFormData(row.id);
console.log({ ...row, url: [row.url] });
editModalRef.value?.setFormData({ ...row, url: [row.url] });
}
1;
// 其他工具栏
function handleToolbarClick(name: string) {
console.log(name);
if (name === "custom1") {
ElMessage.success("点击了自定义1按钮");
}
}
// 其他操作列
async function handleOperatClick(data: IOperatData) {
console.log(data);
}
</script>