新增 菜单界面[待完善]

This commit is contained in:
小肥羊 2025-08-08 18:32:04 +08:00
parent ac2a6caa1e
commit 78d85e8aac
10 changed files with 1445 additions and 1 deletions

19
src/api/enum.ts Normal file
View File

@ -0,0 +1,19 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getenum(type) {
return http.request<Res<any>>("get", `public/enum/${type}`);
}
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getenumDic(type) {
return http.request<Res<any>>("get", `public/enum/${type}/Dic`);
}

31
src/api/hTable.ts Normal file
View File

@ -0,0 +1,31 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
export class hTableAPI {
url = "";
/** 构造函数 */
constructor(url) {
this.url = url;
}
PageList(data = {}) {
return http.request<Res<any>>("post", `${this.url}/PageList`, { data });
}
Info(id, tag = {}) {
const pUrl = `${this.url}/${id}`;
let getUrl = pUrl;
for (const key in tag) {
const el = tag[key];
getUrl += (getUrl === pUrl ? "?" : "&") + key + "=" + el;
}
return http.request<Res<any>>("get", getUrl);
}
edit(data) {
return http.request<Res<any>>("post", `${this.url}/Edit`, { data });
}
delete(data) {
return http.request<Res<any>>("post", `${this.url}/Del`, { data });
}
querycombo(data) {
return http.request<Res<any>>("post", `${this.url}/QueryCombo`, { data });
}
}

24
src/api/menu.ts Normal file
View File

@ -0,0 +1,24 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
// 定义菜单项接口
export interface MenuItem {
id: number;
name: string;
path?: string;
isButton: boolean;
title: string;
icon?: string;
auths?: string;
rank: number;
showLink: boolean;
parentId: number;
children?: MenuItem[];
}
/**
* @description
* @return {object}
*/
export function MenuAll() {
return http.request<Res<MenuItem[]>>("get", `Menu/All`);
}

View File

@ -0,0 +1,182 @@
export interface Dialog {
/* 对话框是否可见 */
visible: boolean;
/* 是否显示关闭按钮 */
close: boolean;
/* 对话框标题 */
title: string;
/* 对话框宽度 */
width: string;
/**自定义弹窗数据 */
custom: {
/* 自定义对话框高度 */
height: string;
/* 自定义对话框数据 */
data: any[];
/* 自定义组件路径 */
src?: string;
/* 自定义配置项 */
custom: Record<string, any>;
/* 自定义对话框是否可见 */
visible: boolean;
/* 异步加载组件 */
component: any;
};
edit: {
/* 编辑项ID */
id: number;
/* 编辑对话框标题 */
title: string;
/* 编辑对话框是否可见 */
visible: boolean;
row?: any;
tagData?: any;
};
}
/* 按钮自定义配置 */
export interface ButtonCustomConfig {
/* 弹出框标题 */
title: string;
/* 组件路径 */
src: string;
/* 弹框宽度 */
width: string;
/* 弹框高度 */
height: string;
}
/* 操作按钮配置 */
export interface OperationButton {
/* 是否为头部按钮 */
topBtn: boolean;
/* 是否显示 */
show?: boolean;
/* 按钮文本 */
label: string;
/* 按钮类型 */
btnType: "add" | "edit" | "del" | "custom";
/* 按钮样式 */
btnStyle?: "success" | "danger";
/* 自定义按钮配置 */
custom?: ButtonCustomConfig;
}
/* 字段设置项 */
export interface FieldSetting {
/**map 时Value的取值的属性 */
mapValue?: string;
/**map 时label的取值的属性 */
maplabel?: string;
/** 数据源 请求方式 */
datasourceStr?: string;
/**
*
* @param value
* @param row
* @returns url
*/
imgUrl?: (value: any, row: any) => string;
/* 数据源 */
datasource?: Array<{
Value: any;
Text: string;
}>;
}
///* 表格列配置 */
//export interface TableEditColumn {}
/* 表格列配置 */
export interface TableColumn {
/* 显示标签 */
label: string;
/* 是否可搜索 */
search: boolean;
/* 搜索类型 */
searchType?:
| "Equal"
| "NoEqual"
| "Like"
| "GreaterThan"
| "LessThan"
| "NoLike";
/* 是否允许添加 */
add: boolean;
/* 是否允许修改 */
edit: boolean;
/* 列宽度 */
width?: string;
/* 字段类型 */
type?: string;
/** 是否多选 */
multiple?: boolean;
/** 编辑时显示列 */
editShow?: boolean;
rules?: any | Array<any>;
/** 显示列 */
show?: boolean;
/* 字段设置 */
setting?: FieldSetting;
/* 修改时的编辑值 */
valueE?: Array<string> | string;
/* 查询值 */
value?: Array<string> | string;
/** textarea编辑时的行数 */
editRows?: number;
/**编辑时值发生变化 */
change?: () => void;
/**列值初始化时 如何获取默认取对应列*/
custom?: (row: any) => string;
}
/* 分页数据 */
export interface PageData {
/* 总条数 */
total: number;
}
/* 搜索条件 */
export interface SearchConditions {
/* 是否显示搜索 */
show: boolean;
/* 当前页码 */
PageIndex: number;
/* 每页大小 */
PageSize: number;
/* 排序字段 */
OrderBy: string;
/* 默认查询条件 */
defaultConditions: any[];
/* 查询条件 */
Conditions: any[];
}
/* 表格配置 */
export interface TableConfig {
/* 搜索回调函数 */
searchCallback?: (s: SearchConditions) => void;
/* 新增/修改回调函数 */
editCallback?: (from: any) => void;
/* API地址 */
apiUrl: string;
/* 是否显示选择列 */
selectColumn: boolean;
/* 搜索配置 */
search: SearchConditions;
/* 是否显示操作列 */
operationColumn: boolean;
/* 操作按钮配置 */
operationColumnData: OperationButton[];
/* 列配置 */
column: Record<string, TableColumn>;
/* 表格数据 */
data: any[];
/**显示头部操作按钮 */
operationTop?: boolean;
/* 分页数据 */
pageData: PageData;
/* 选中行 */
selectRows: any[];
/* 是否显示边框 */
border: boolean;
}

View File

@ -0,0 +1,218 @@
<script setup lang="ts">
import { onMounted, PropType, ref } from "vue";
import { hTableAPI } from "@/api/hTable";
import { TableConfig, TableColumn } from "./hTable";
import { FormInstance } from "element-plus";
import { ElMessage, ElMessageBox } from "element-plus";
const props = defineProps({
//** */
id: {
type: Number,
default: -1
},
tableData: {
type: Object as PropType<TableConfig>,
default: null
},
row: {
type: Object,
default: null
},
tagData: {
type: Object,
default: () => {}
}
});
const emit = defineEmits(["handlePagedCallback"]);
const editFormRef = ref<FormInstance>();
const column: Record<string, TableColumn> = {};
const editData = ref({
frorm: {},
isedit: props.id !== -1,
table: props.tableData,
rules: [
{
required: true,
message: "不能为空",
trigger: "blur"
}
],
formLabelWidth: "120px",
size: "small",
loading: false
});
const Api = new hTableAPI(editData.value.table.apiUrl);
onMounted(() => {
intiColumn();
fetchInitData();
fetchFormData();
});
function execute(obj, btn) {
return eval(obj);
}
function intiColumn() {
for (const key in editData.value.table.column) {
const element = editData.value.table.column[key];
if (editData.value.isedit) {
if (element.edit) {
column.value[key] = element;
}
} else {
if (element.add) {
column.value[key] = element;
}
}
}
}
function handlePagedCallback() {
emit("handlePagedCallback"); //
}
function handleSubmitForm() {
editFormRef.value.validate(valid => {
if (!valid) {
return;
}
editData.value.loading = true;
let form = {};
if (editData.value.isedit) {
form = props.row;
}
for (const key in column.value) {
const element = column.value[key];
if (element.valueE !== null && element.valueE !== "") {
form[key] = element.valueE;
}
}
if (editData.value.table.editCallback) {
editData.value.table.editCallback(form);
}
Api.edit(form).then(res => {
editData.value.loading = false;
if (res.code === 200) {
ElMessage.success("操作成功");
setTimeout(handlePagedCallback, 0);
} else {
ElMessage.error(res.message);
}
});
});
}
function handleResetForm() {
for (const key in column.value) {
let item = column.value[key];
if (Array.isArray(item.valueE)) {
item.valueE = [];
} else if (typeof item.valueE === "number") {
item.valueE = 0;
} else if (typeof item.valueE === "boolean") {
item.valueE = false;
} else {
item.valueE = "";
}
}
}
function fetchInitData() {}
function fetchFormData() {
editData.value.loading = false;
handleResetForm();
if (editData.value.isedit) {
Api.Info(props.id).then(res => {
if (res.code === 200) {
editData.value.frorm = res.data;
for (const key in column.value) {
const element = column.value[key];
element.valueE = res.data[key];
}
}
editData.value.loading = true;
});
} else {
editData.value.loading = true;
}
}
</script>
<template>
<div>
<el-form
v-if="editData.loading"
ref="editFormRef"
:model="editData.table.column"
:label-width="editData.formLabelWidth"
size="small"
clearable
>
<el-form-item
v-for="(o, k, i) in column"
v-show="execute(o['editShow'], o)"
:key="i"
:rules="o.rules"
:prop="'' + k + '.valueE'"
:label="o.label"
>
<div v-if="o.type.trim() == 'datetime'">
<el-date-picker
v-model="o.valueE"
format="yyyy-MM-dd HH:mm:ss"
value-format="yyyy-MM-dd HH:mm:ss"
type="datetime"
:placeholder="o.label"
style="width: 100%"
@change="o.change"
/>
</div>
<div v-else-if="o.type.trim() == 'dropdown'">
<el-select
v-model="o.valueE"
:multiple="o.multiple"
clearable
filterable
:placeholder="o.label"
style="width: 100%"
@change="o.change"
>
<el-option
v-for="item in o.setting.datasource"
:key="item.Value"
autocomplete="off"
:label="item[o.setting.maplabel]"
:value="item[o.setting.mapValue]"
/>
</el-select>
</div>
<div v-else-if="o.type.trim() == 'textarea'">
<el-input
v-model="o.valueE as string"
:rows="o.editRows || 4"
type="textarea"
:placeholder="o.label"
@change="o.change"
/>
</div>
<div v-else-if="o.type.trim() == 'switch'">
<el-switch
v-model="o.valueE as string"
active-text="启用"
inactive-text="禁用"
@change="o.change"
/>
</div>
<div v-else>
<el-input v-model="o.valueE as string" :placeholder="o.label" />
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="!editData.loading"
@click="handleSubmitForm()"
>立即提交</el-button
>
<el-button @click="handleResetForm()">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>

View File

@ -0,0 +1,597 @@
<script setup lang="ts">
import {
ref,
unref,
watch,
reactive,
computed,
onMounted,
nextTick,
onUnmounted,
getCurrentInstance,
onBeforeMount,
PropType
} from "vue";
import { Search } from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { defineAsyncComponent, AsyncComponentLoader } from "vue";
import { Dialog, TableConfig } from "./hTable";
import hTableEdit from "./hTableEdit.vue";
import { hTableAPI } from "@/api/hTable";
import { getenum } from "@/api/enum";
const props = defineProps({
//** */
Row: {
type: Object as PropType<any>,
default: () => ({})
},
tableConfig: {
type: Object as PropType<TableConfig>,
default: () => ({})
}
});
const table = ref<TableConfig>(props.tableConfig);
onBeforeMount(() => {
/* 初始化系统配置 */
nextTick(async () => {
intdata();
Api = new hTableAPI(table.value.apiUrl);
init.value = true;
tableShow.value = true;
await fetchInitData();
fetchPagedData();
});
});
onMounted(() => {
nextTick(async () => {
// appStyle();
});
});
onUnmounted(() => {});
// defineExpose({
// initTable
// });
const tableShow = ref(false);
let Api: hTableAPI = null;
const instance = getCurrentInstance();
const init = ref(false);
const tableHeight = ref(0);
const dialog = ref<Dialog>({
visible: false,
close: true,
title: "",
width: "500px", //
custom: {
height: "85vh",
data: [],
custom: {},
component: null, //
visible: false //
},
edit: {
id: -1,
title: "", // title
visible: false //
}
});
const appB = ref<HTMLElement>(null);
const appB_S = ref<HTMLElement>(null);
function appStyle() {
if (tableHeight.value !== 0) return;
tableHeight.value =
appB.value.parentElement.parentElement.offsetHeight -
145 -
appB_S.value.offsetHeight;
return tableHeight;
}
function intdata() {
if (!table.value.data) table.value.data = [];
if (!table.value.selectRows) table.value.selectRows = [];
if (table.value.border == null) table.value.border = true;
if (!table.value.pageData) table.value.pageData = { total: 0 };
if (table.value.operationTop === undefined) table.value.operationTop = true;
// column
for (const key in table.value.column) {
const element = table.value.column[key];
// Vue 3 $set
if (element.valueE === undefined)
element.valueE = element.multiple ? [] : "";
if (element.value === undefined) element.value = element.multiple ? [] : "";
if (!element.type) element.type = "string";
if (element.show === undefined) element.show = true;
if (element.editShow === undefined) element.editShow = true;
if (!element.setting)
element.setting = { datasource: [], mapValue: "Value", maplabel: "Text" };
if (!element.setting.mapValue) element.setting.mapValue = "Value";
if (!element.setting.maplabel) element.setting.maplabel = "Text";
if (!element.change) element.change = () => {};
}
// operationColumnData
for (const key in table.value.operationColumnData) {
const element = table.value.operationColumnData[key];
if (element.show === undefined) element.show = true;
}
}
function rowKeyFun(row) {
return row.customId;
}
function execute(obj, scope, btn) {
if (Object.prototype.toString.call(obj) === "[object Function]")
return obj(scope, btn);
return eval(obj);
}
function getOperationColumnWidth() {
let hFontSize = getComputedStyle(window.document.documentElement)[
"font-size"
].replace("px", "");
let defWidth = 10 + 2 + 30;
let width = eval(
table.value.operationColumnData
.filter(s => !s.topBtn && s.show)
.map(s => defWidth + s.label.length * (hFontSize || 16))
.join("+")
);
width = width < 100 ? 100 : width;
width = width > 320 ? 320 : width;
return width + "px";
}
function getbtnClick(obj, row) {
row = row ? [row] : table.value.selectRows;
if (obj.btnType === "add") {
return handleAdd();
} else if (obj.btnType === "edit") {
return handleEdit(obj, row);
} else if (obj.btnType === "del") {
return handleDelete(obj, row);
} else if (obj.btnType === "custom") {
return handleCustom(obj, row, obj.custom);
} else {
return obj.click(obj, row, handleReloadPaged);
}
}
function handleAdd() {
dialog.value.edit.id = -1;
dialog.value.title = "添加";
dialog.value.custom.visible = false;
dialog.value.edit.visible = true;
dialog.value.visible = true;
dialog.value.width = "500px";
}
function handleEdit(obj, row) {
dialog.value.edit.id = row[0].Id;
dialog.value.edit.row = row[0];
dialog.value.edit.tagData = obj.tagData;
dialog.value.title = obj.label || "修改";
dialog.value.edit.visible = true;
dialog.value.custom.visible = false;
dialog.value.visible = true;
dialog.value.width = "500px";
}
function handleCustom(obj, row, custom) {
dialog.value.custom.data = row || [];
dialog.value.custom.custom = custom || [];
//
dialog.value.custom.component = defineAsyncComponent({
loader: () => import(/* @vite-ignore */ `../${custom.src}.vue`)
});
dialog.value.width = custom.width;
dialog.value.title = custom.title;
dialog.value.edit.visible = false;
dialog.value.custom.visible = true;
if (custom.height !== undefined) {
dialog.value.custom.height = custom.height;
} else {
dialog.value.custom.height = "600px";
}
dialog.value.visible = true;
}
//
function handleDelete(obj, row) {
if (row.length === 0) {
ElMessage.warning("未勾选记录");
return;
}
const ids: any[] = [];
row.forEach(it => {
ids.push(it.Id);
});
ElMessageBox.confirm("此操作将永久删除勾选记录, 是否继续?").then(() => {
Api.delete(ids).then(res => {
if (res.code === 200) {
handleReloadPaged();
ElMessage.success("删除成功");
} else {
ElMessage.error(res.message);
}
});
});
}
//
function handleAddCallback() {
Api.url = table.value.apiUrl;
dialog.value.visible = false;
dialog.value.edit.visible = false;
dialog.value.custom.visible = false;
handleReloadPaged();
}
function tableClose() {
handleAddCallback();
}
function pageSizeChange(o) {
table.value.search.PageSize = o;
fetchPagedData();
}
function pageIndexChange(o) {
table.value.search.PageIndex = o - 1;
fetchPagedData();
}
//
function handleSelectionChange(selection) {
console.log("selection-change", selection);
table.value.selectRows = selection;
}
//
function handleReloadPaged(reload = true) {
if (
table.value.search === undefined ||
table.value.search.PageIndex === undefined
) {
table.value.search.PageIndex = 0;
table.value.search.PageSize = 20;
}
table.value.search.Conditions = [];
for (let name in table.value.column) {
if (
!table.value.column[name].search ||
table.value.column[name].value === undefined ||
table.value.column[name].value === null ||
table.value.column[name].value === ""
) {
continue;
}
let data: any = { ConditionalType: 0 };
if (table.value.column[name].type === "datetime") {
// data.CSharpTypeName = 'DateTime'
data.ConditionalType = 8; // '2023-10-07%'
} else if (table.value.column[name].type === "switch") {
data.CSharpTypeName = "Boolean";
} else if (table.value.column[name].type === "string") {
data.ConditionalType = "Like";
}
data.FieldName = name.charAt(0).toUpperCase() + name.slice(1);
data.FieldValue = table.value.column[name].value;
if (table.value.column[name].searchType != undefined) {
data.ConditionalType = table.value.column[name].searchType || 0;
}
table.value.search.Conditions.push(data);
}
if (!reload && table.value.search.Conditions.length > 0) {
table.value.search.PageIndex = 0;
}
if (table.value.searchCallback) {
table.value.searchCallback(table.value.search);
}
fetchPagedData();
}
//
async function fetchInitData() {
for (const key in table.value.column) {
const element = table.value.column[key];
if (element.type === "dropdown") {
if (!element.setting.datasource) {
//
let rdata = await eval(element.setting.datasourceStr);
element.setting.datasource = rdata.data;
console.log(key + " " + element.setting.datasourceStr, rdata);
}
}
if (
element.custom == undefined &&
(element.type === "switch" ||
element.type === "dropdown" ||
element.type === "string" ||
element.type === undefined)
) {
element.custom = row => {
let sc = element.setting.datasource.find(
s => s[element.setting.mapValue] + "" == row[key] + ""
);
return !sc ? row[key] : sc[element.setting.maplabel];
};
} else if (element.custom == undefined) {
element.custom = row => row[key];
}
}
setTimeout(() => {
init.value = true;
appStyle();
}, 0);
}
function showTips(item, value) {
if (item.width == undefined) {
return false;
}
return (
getByteLen(item.custom(value) + "") * 16 < item.width.replace("px", "")
);
}
function getByteLen(str) {
let len = 0;
for (let i = 0; i < str.length; i++) {
str.charCodeAt(i) < 256 ? (len += 1) : (len += 2);
}
return len;
}
//
function fetchPagedData() {
for (const iterator of table.value.search.defaultConditions) {
if (!iterator) continue;
if (!table.value.search.Conditions.find(s => s == iterator)) {
table.value.search.Conditions.push(iterator);
}
}
Api.PageList(table.value.search).then(res => {
if (res.code === 200) {
table.value.data = res.data.data.map((s, i) => {
return { ...s, customId: i };
});
table.value.pageData = res.data;
}
});
}
</script>
<template>
<div v-if="tableShow" ref="appB" class="app-container container-h">
<div ref="appB_S" class="search-container1">
<!-- 搜索项目 -->
<el-form v-if="table.search.show" :inline="true" :model="table.search">
<el-form-item
v-for="(o, n, i) in table.column"
v-show="o.search"
:key="i"
>
<el-date-picker
v-if="o.type.trim() == 'datetime'"
v-model="o.value"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
type="date"
:placeholder="o.label"
style="width: 100%"
/>
<el-select
v-else-if="o.type.trim() == 'dropdown'"
v-model="o.value"
:multiple="o.multiple"
clearable
filterable
:placeholder="o.label"
style="width: 100%"
>
<el-option
v-for="item in o.setting.datasource"
:key="item.Value"
autocomplete="off"
:label="item[o.setting.maplabel]"
:value="item[o.setting.mapValue]"
/>
</el-select>
<el-select
v-else-if="o.type.trim() == 'switch'"
v-model="o.value"
clearable
filterable
:placeholder="o.label"
style="width: 100%"
>
<el-option autocomplete="off" :label="'启用'" :value="true" />
<el-option autocomplete="off" :label="'禁用'" :value="false" />
</el-select>
<div v-else-if="o.type.trim() == 'img'" v-show="false" />
<el-input v-else v-model="o.value as string" :placeholder="o.label" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
:icon="Search"
@click="handleReloadPaged(false)"
>查询</el-button
>
</el-form-item>
</el-form>
</div>
<div v-if="table.operationTop" class="toolbar-container">
<!-- 头部按钮组 -->
<el-button
v-for="(e, i) in table.operationColumnData.filter(s => s.topBtn)"
v-show="execute(e['show'], {}, e)"
:key="i"
:type="e.btnStyle || 'info'"
@click="getbtnClick(e, null)"
>{{ e.label }}</el-button>
</div>
<el-table
v-if="init"
fit
:height="tableHeight"
:data="table.data"
:border="table.border"
:highlight-current-row="true"
style="width: 100%"
:row-key="rowKeyFun"
@selection-change="handleSelectionChange"
>
<el-table-column v-if="table.selectColumn" type="selection" width="40" />
<el-table-column
v-if="
table.operationColumn &&
table.operationColumnData.filter(s => !s.topBtn).length > 0
"
label="操作"
:width="getOperationColumnWidth()"
>
<template v-slot="scope">
<div class="columnTemplate">
<el-button
v-for="(e, i) in table.operationColumnData.filter(s => !s.topBtn)"
v-show="execute(e['show'], scope, e)"
:key="i"
class="btn"
text
:type="e.btnStyle || 'primary'"
@click="getbtnClick(e, scope.row)"
>
{{ e.label }}</el-button
>
</div>
<!-- 行内按钮组 -->
</template>
</el-table-column>
<el-table-column
v-for="(item, name, i) of table.column"
v-show="item.show"
:key="i"
:prop="name"
:label="item.label"
:width="item.width"
>
<template v-slot="scope">
<div class="columnTemplate">
<!-- 暂未实现 -->
<div v-if="item.type.trim() == 'math'">
{{ item.custom(scope.row) }}
</div>
<!-- <ShowMathJax
v-if="item.type.trim() == 'math'"
style="zoom: 0.8"
:str="item.custom(scope.row)"
:divId="'MathJax_' + scope.row.Id"
/> -->
<el-image
v-else-if="item.type.trim() == 'img'"
style="width: 300px; height: 100px"
fit="fill"
:src="item.setting.imgUrl(scope.row[name], scope.row)[0]"
:preview-src-list="
Array.from(item.setting.imgUrl(scope.row[name], scope.row))
"
>
<template v-slot:error>
<div class="image-slot">
{{ item.setting.imgUrl(scope.row[name], scope.row) }}
</div>
</template>
</el-image>
<el-tooltip
v-else-if="item.type.trim() != 'img'"
popper-class="maxWidth600px"
:disabled="showTips(item, scope.row)"
:content="item.custom(scope.row) + ''"
placement="top"
effect="light"
>
<div class="tab_tip" :style="{ width: item.width }">
{{ item.custom(scope.row) }}
</div>
</el-tooltip>
<!-- <p ></p> -->
</div>
</template>
</el-table-column>
</el-table>
<div style="display: flex; align-items: center; justify-content: center">
<el-pagination
:current-page="table.search.PageIndex + 1"
:page-sizes="[20, 40, 80, 100]"
:page-size="table.search.PageSize"
layout="prev, pager, next,sizes, total"
:total="table.pageData.total"
@size-change="pageSizeChange"
@current-change="pageIndexChange"
/>
</div>
<div class="dialog-container">
<el-dialog
v-if="dialog.visible"
ref="elDialog"
v-model:visible="dialog.visible"
:class="dialog.title ? '' : 'noHeader'"
:title="dialog.title"
:width="dialog.width"
:close-on-click-modal="false"
:close-on-press-escape="dialog.close"
:before-close="tableClose"
append-to-body
>
<hTableEdit
v-if="dialog.edit.visible"
:id="dialog.edit.id"
:data="table"
:tagData="dialog.edit.tagData"
:row="dialog.edit.row"
@handlePagedCallback="handleAddCallback"
/>
/>
<component
:is="dialog.custom.component"
v-if="dialog.custom.visible"
ref="custom"
:style="{ height: 'calc( ' + dialog.custom.height + ' - 84px )' }"
:iscomponent="true"
:custom="dialog.custom.custom"
:CancelCallback="handleAddCallback"
:data="dialog.custom.data"
@handlePagedCallback="handleAddCallback"
/>
</el-dialog>
</div>
</div>
</template>
<style scoped>
.columnTemplate .btn {
margin-left: 0 !important;
}
.columnTemplate {
display: flex;
gap: 5px;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-ms-flex-direction: row;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
align-content: flex-start;
}
.sty .tab_tip {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.toolbar-container {
padding-bottom: 5px;
}
.maxWidth600px {
max-width: 600px;
}
</style>

View File

@ -20,6 +20,24 @@ export default {
title: "首页", title: "首页",
showLink: VITE_HIDE_HOME === "true" ? false : true showLink: VITE_HIDE_HOME === "true" ? false : true
} }
},
{
path: "/school",
name: "school",
component: () => import("@/views/school/index.vue"),
meta: {
title: "学校",
showLink: true
}
},
{
path: "/menu",
name: "Menu",
component: () => import("@/views/menu/index.vue"),
meta: {
title: "菜单",
showLink: true
}
} }
] ]
} satisfies RouteConfigsTable; } satisfies RouteConfigsTable;

View File

@ -26,7 +26,6 @@ export const usePermissionStore = defineStore("pure-permission", {
actions: { actions: {
/** 组装整体路由生成的菜单 */ /** 组装整体路由生成的菜单 */
handleWholeMenus(routes: any[]) { handleWholeMenus(routes: any[]) {
debugger;
this.wholeMenus = filterNoPermissionTree( this.wholeMenus = filterNoPermissionTree(
filterTree(ascending(this.constantMenus.concat(routes))) filterTree(ascending(this.constantMenus.concat(routes)))
); );

236
src/views/menu/index.vue Normal file
View File

@ -0,0 +1,236 @@
<template>
<div>
<div style="padding-bottom: 5px;">
<el-button type="primary"> 分配权限 </el-button>
</div>
<el-tree
:data="treeData"
:props="treeProps"
node-key="id"
:default-expand-all="true"
:highlight-current="true"
show-checkbox
class="menu-tree"
>
<template #default="{ node, data }">
<div class="menu-node">
<i v-if="data.icon" :class="data.icon" class="menu-icon"></i>
<span class="menu-title">{{ data.title }}</span>
<span class="menu-path" v-if="data.path">{{ data.path }}</span>
</div>
<div style="display: flex; gap: 6px">
<el-button type="success" link @click="() => {}"> 添加 </el-button>
<el-button type="primary" link @click="() => {}"> 编辑 </el-button>
<el-button type="danger" link @click="() => {}"> 删除 </el-button>
</div>
</template>
</el-tree>
</div>
</template>
<script setup lang="ts">
import { MenuAll, MenuItem } from "@/api/menu";
import { ElMessage } from "element-plus";
import { ref, computed, onMounted } from "vue";
defineOptions({
name: "Menu"
});
// API
const mockMenuData: MenuItem[] = [
{
id: 1,
name: "dashboard",
path: "/dashboard",
isButton: false,
title: "控制台",
icon: "el-icon-monitor",
rank: 1,
showLink: true,
parentId: 0
},
{
id: 2,
name: "system",
path: "/system",
isButton: false,
title: "系统管理",
icon: "el-icon-setting",
rank: 2,
showLink: true,
parentId: 0
},
{
id: 3,
name: "user",
path: "/system/user",
isButton: false,
title: "用户管理",
icon: "el-icon-user",
rank: 1,
showLink: true,
parentId: 2
},
{
id: 4,
name: "role",
path: "/system/role",
isButton: false,
title: "角色管理",
icon: "el-icon-s-custom",
rank: 2,
showLink: true,
parentId: 2
},
{
id: 5,
name: "createUser",
isButton: true,
title: "创建用户",
rank: 1,
showLink: true,
parentId: 3
}
];
//
const convertToTree = (menus: MenuItem[]): MenuItem[] => {
const menuMap = new Map<number, MenuItem>();
const tree: MenuItem[] = [];
// children
menus.forEach(menu => {
menuMap.set(menu.id, { ...menu, children: [] });
});
//
menuMap.forEach(menu => {
if (menu.parentId === 0) {
tree.push(menu);
} else {
const parent = menuMap.get(menu.parentId);
if (parent) {
parent.children!.push(menu);
}
}
});
//
const sortChildren = (nodes: MenuItem[]) => {
nodes.sort((a, b) => a.rank - b.rank);
nodes.forEach(node => {
if (node.children && node.children.length > 0) {
sortChildren(node.children);
}
});
};
sortChildren(tree);
return tree;
};
//
const treeData = ref<MenuItem[]>([]);
//
const treeProps = {
label: "title",
children: "children"
};
// API
const fetchMenuData = async (): Promise<MenuItem[]> => {
// API:
// const response = await fetch('/api/menus')
// return await response.json()
// 使
return new Promise(resolve => {
setTimeout(() => resolve(mockMenuData), 300);
});
};
onMounted(async () => {
const flatData = await MenuAll();
if (flatData.code === 200) {
treeData.value = convertToTree(flatData.data);
} else {
ElMessage.error(flatData.message);
}
});
</script>
<style scoped>
.menu-tree {
padding: 15px;
background: #fff;
border-radius: 4px;
}
.menu-node {
display: flex;
align-items: center;
width: 100%;
padding: 5px 0;
}
.menu-icon {
margin-right: 8px;
font-size: 16px;
color: #409eff;
}
.menu-title {
font-size: 14px;
margin-right: 10px;
}
.menu-path {
font-size: 12px;
color: #909399;
margin-right: 10px;
flex-grow: 1;
}
.menu-tree {
padding: 20px; /* 增加内边距 */
background: #fff;
border-radius: 4px;
font-size: 16px; /* 增大整体字体 */
}
/* 使用深度选择器修改树节点样式 */
:deep(.el-tree-node) {
margin-bottom: 12px; /* 增加节点间距 */
}
:deep(.el-tree-node__content) {
height: 48px; /* 增加节点高度 */
padding: 10px 0; /* 增加垂直内边距 */
}
.menu-node {
display: flex;
align-items: center;
width: 100%;
padding: 8px 0; /* 增加内边距 */
}
.menu-icon {
margin-right: 12px; /* 增加图标右边距 */
font-size: 20px; /* 增大图标 */
color: #409eff;
}
.menu-title {
font-size: 18px; /* 增大标题字体 */
margin-right: 15px; /* 增加右边距 */
font-weight: 500; /* 加粗字体 */
}
.menu-path {
font-size: 16px; /* 增大路径字体 */
color: #909399;
margin-right: 15px; /* 增加右边距 */
flex-grow: 1;
}
</style>

120
src/views/school/index.vue Normal file
View File

@ -0,0 +1,120 @@
<script setup lang="ts">
import ahTable from "@/components/hTable/index.vue";
import { TableConfig } from "@/components/hTable/hTable";
import { onMounted, ref } from "vue";
defineOptions({
name: "School"
});
onMounted(() => {});
function searchCallback(data) {
let c = data.Conditions.find(s => s.FieldName === "Enable");
if (c) {
if (c.FieldValue == "true") {
c.FieldValue = 1;
} else {
c.FieldValue = 0;
}
}
}
const table = ref<{ initTable: (config: TableConfig) => void }>(null);
const tableData: TableConfig = {
apiUrl: "School",
selectColumn: false, //
border: false, //
searchCallback: searchCallback,
search: {
//
show: true,
PageIndex: 0,
PageSize: 20,
OrderBy: "CreateTime", //
defaultConditions: [], //
Conditions: []
},
operationColumn: true, //
operationColumnData: [
{
//
topBtn: false, //
label: "修改",
btnType: "edit" // add edit del custom
},
{
topBtn: true, //
show: true,
label: "新增",
btnType: "add", // add edit del custom
btnStyle: "success" // topBtn: true success danger
},
// {
// topBtn: true, //
// label: "",
// btnType: "custom", // add edit del custom
// btnStyle: "success", // topBtn: true success danger
// custom: {
// // custom
// title: "", // title
// src: "school/SchoolEdit", //
// width: "550px", //
// height: "300px" //
// }
// },
{
topBtn: false, //
show: true,
label: "删除",
btnType: "del", // add edit del
btnStyle: "danger" // topBtn: true success danger
}
],
column: {
//
id: {
label: "编号",
search: true,
add: true, //
edit: true, //
width: "150px"
},
name: {
label: "学校名称",
width: "300px",
search: true,
searchType: "Like",
add: true, //
edit: true //
},
loc: {
label: "地区",
width: "300px",
search: true,
custom: row => `${row.pname}-${row.cname}-${row.rname}`,
add: true, //
edit: true //
},
enable: {
label: "启用",
type: "dropdown",
search: true,
add: true, //
edit: true, //
setting: {
datasource: [
{ Value: "true", Text: "√" },
{ Value: "false", Text: "X" }
]
}
}
},
data: [],
pageData: {
total: 0
},
selectRows: []
};
</script>
<template>
<div><ahTable ref="table" :tableConfig="tableData" /></div>
</template>