This commit is contained in:
sheng
2023-07-25 15:25:57 +08:00
parent 8f57a240db
commit 7ccbcebe77
373 changed files with 41578 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/area/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,225 @@
import * as api from './api';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '/@/utils/message';
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
/**
* 懒加载
* @param row
* @returns {Promise<unknown>}
*/
const loadContentMethod = (tree: any, treeNode: any, resolve: Function) => {
pageRequest({ pcode: tree.code }).then((res: APIResponseData) => {
resolve(res.data);
});
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
},
pagination: {
show: false,
},
table: {
rowKey: 'id',
lazy: true,
load: loadContentMethod,
treeProps: { children: 'children', hasChildren: 'hasChild' },
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
// pcode: {
// title: '父级地区',
// show: false,
// search: {
// show: true,
// },
// type: 'dict-tree',
// form: {
// component: {
// showAllLevels: false, // 仅显示最后一级
// props: {
// elProps: {
// clearable: true,
// showAllLevels: false, // 仅显示最后一级
// props: {
// checkStrictly: true, // 可以不需要选到最后一级
// emitPath: false,
// clearable: true,
// },
// },
// },
// },
// },
// },
name: {
title: '名称',
search: {
show: true,
},
treeNode: true,
type: 'input',
column:{
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '名称必填项' },
],
component: {
placeholder: '请输入名称',
},
},
},
code: {
title: '地区编码',
search: {
show: true,
},
type: 'input',
column:{
minWidth: 90,
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '地区编码必填项' },
],
component: {
placeholder: '请输入地区编码',
},
},
},
pinyin: {
title: '拼音',
search: {
disabled: true,
},
type: 'input',
column:{
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '拼音必填项' },
],
component: {
placeholder: '请输入拼音',
},
},
},
level: {
title: '地区层级',
search: {
disabled: true,
},
type: 'input',
column:{
minWidth: 100,
},
form: {
disabled: false,
rules: [
// 表单校验规则
{ required: true, message: '拼音必填项' },
],
component: {
placeholder: '请输入拼音',
},
},
},
initials: {
title: '首字母',
column:{
minWidth: 100,
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '首字母必填项' },
],
component: {
placeholder: '请输入首字母',
},
},
},
enable: {
title: '是否启用',
search: {
show: true,
},
type: 'dict-radio',
column: {
minWidth:90,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
onChange: compute((context) => {
return () => {
api.UpdateObj(context.row).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
};
}),
},
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
},
},
};
};

View File

@@ -0,0 +1,18 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts" setup name="areas">
import { ref, onMounted } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>

View File

@@ -0,0 +1,61 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import XEUtils from 'xe-utils';
export const apiPrefix = '/api/system/system_config/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}
/*
获取所有的model及字段信息
*/
export function GetAssociationTable() {
return request({
url: apiPrefix + 'get_association_table/',
method: 'get',
params: {},
});
}
export function saveContent(data: any) {
return request({
url: apiPrefix + 'save_content/',
method: 'put',
data: data,
});
}

View File

@@ -0,0 +1,161 @@
<template>
<div style="padding: 20px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="所属分组" prop="parent">
<el-select v-model="form.parent" placeholder="请选择分组" clearable>
<el-option :label="item.title" :value="item.id" :key="index" v-for="(item, index) in parentOptions"></el-option>
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入" clearable></el-input>
</el-form-item>
<el-form-item label="key值" prop="key">
<el-input v-model="form.key" placeholder="请输入" clearable></el-input>
</el-form-item>
<el-form-item label="表单类型" prop="form_item_type">
<el-select v-model="form.form_item_type" placeholder="请选择" clearable>
<el-option :label="item.label" :value="item.value" :key="index" v-for="(item, index) in dictionary('config_form_type')"></el-option>
</el-select>
</el-form-item>
<el-form-item
v-if="[4, 5, 6].indexOf(form.form_item_type) > -1"
label="字典key"
prop="setting"
:rules="[{ required: true, message: '不能为空' }]"
>
<el-input v-model="form.setting" placeholder="请输入dictionary中key值" clearable></el-input>
</el-form-item>
<div v-if="[13, 14].indexOf(form.form_item_type) > -1">
<associationTable ref="associationTableRef" v-model="form.setting" @updateVal="associationTableUpdate"></associationTable>
</div>
<el-form-item label="校验规则">
<el-select v-model="form.rule" multiple placeholder="请选择(可多选)" clearable>
<el-option :label="item.label" :value="item.value" :key="index" v-for="(item, index) in ruleOptions"></el-option>
</el-select>
</el-form-item>
<el-form-item label="提示信息" prop="placeholder">
<el-input v-model="form.placeholder" placeholder="请输入" clearable></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="99"></el-input-number>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit(formRef)">立即创建</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import * as api from '../api';
import associationTable from './components/associationTable.vue';
import {ref, reactive, onMounted, inject} from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { successMessage } from '/@/utils/message';
import { dictionary } from '/@/utils/dictionary';
let form: any = reactive({
parent: null,
title: null,
key: null,
form_item_type: '',
rule: null,
placeholder: null,
});
const formRef = ref<FormInstance>();
const associationTableRef: any = ref<FormInstance>();
const rules = reactive<FormRules>({
parent: [
{
required: true,
message: '请选择',
},
],
title: [
{
required: true,
message: '请输入',
},
],
key: [
{
required: true,
message: '请输入',
},
{
pattern: /^[A-Za-z0-9_]+$/,
message: '请输入数字、字母或下划线',
},
],
form_item_type: [
{
required: true,
message: '请输入',
},
],
});
let parentOptions: any = ref([]);
let ruleOptions = ref([
{
label: '必填项',
value: '{"required": true, "message": "必填项不能为空"}',
},
{
label: '邮箱',
value: '{ "type": "email", "message": "请输入正确的邮箱地址"}',
},
{
label: 'URL地址',
value: '{ "type": "url", "message": "请输入正确的URL地址"}',
},
]);
const getParent = () => {
api
.GetList({
parent__isnull: true,
limit: 999,
})
.then((res: any) => {
parentOptions.value = res.data;
});
};
const refreshView:any = inject('refreshView')
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
api.AddObj(form).then((res: any) => {
if (res.code == 2000) {
successMessage('新增成功');
refreshView()
}
});
} else {
console.log('error submit!', fields);
}
});
};
// 关联表数据更新
const associationTableUpdate = () => {
return new Promise(function (resolve, reject) {
if (associationTableRef) {
if (!associationTableRef.onSubmit()) {
// eslint-disable-next-line prefer-promise-reject-errors
return reject(false);
}
const { formObj } = associationTableRef;
form.setting = formObj;
return resolve(true);
} else {
return resolve(true);
}
});
};
onMounted(() => {
getParent();
});
</script>
<style></style>

View File

@@ -0,0 +1,68 @@
<template>
<div style="padding: 20px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="标题" prop="title">
<el-input v-model="form.title"></el-input>
</el-form-item>
<el-form-item label="key值" prop="key">
<el-input v-model="form.key"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit(formRef)">立即创建</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import * as api from '../api';
import {ref, reactive, inject} from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { successMessage } from '/@/utils/message';
let form = reactive({
title: null,
key: null,
});
const formRef = ref<FormInstance>();
const rules = reactive<FormRules>({
title: [
{
required: true,
message: '请输入',
},
],
key: [
{
required: true,
message: '请输入',
},
{
pattern: /^[A-Za-z0-9]+$/,
message: '只能是英文和数字',
},
],
});
const refreshView:any = inject('refreshView')
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
api.AddObj(form).then((res: any) => {
if (res.code == 2000) {
successMessage('新增成功');
refreshView()
}
});
} else {
console.log('error submit!', fields);
}
});
};
</script>
<style></style>

View File

@@ -0,0 +1,104 @@
<template>
<div>
<el-form :model="formObj" ref="associationRef">
<el-form-item label="关联表" prop="table" :rules="[{ required: true, message: '必填项', trigger: 'blur' }]">
<el-select v-model="formObj.table" filterable clearable placeholder="请选择" @change="handleChange">
<el-option v-for="item in tableOptions" :key="item.table" :label="item.tableName" :value="item.table">
<span style="float: left">{{ item.tableName }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.table }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="显示字段" prop="field" :rules="[{ required: true, message: '必填项', trigger: 'blur' }]">
<el-select v-model="formObj.field" filterable clearable placeholder="请选择">
<el-option v-for="item in labelOptions" :key="item.table" :label="item.title" :value="item.field">
<span style="float: left">{{ item.field }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.title }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="储存字段" prop="primarykey" :rules="[{ required: true, message: '必填项', trigger: 'blur' }]">
<el-select v-model="formObj.primarykey" filterable clearable placeholder="请选择">
<el-option v-for="(item, index) in labelOptions" :key="index" :label="item.title" :value="item.field">
<span style="float: left">{{ item.field }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.title }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="过滤条件" prop="oldSearchField" :rules="[{ required: true, message: '必填项', trigger: 'blur' }]">
<el-select v-model="formObj.oldSearchField" multiple filterable clearable placeholder="请选择" @change="handleSearch">
<el-option v-for="(item, index) in labelOptions" :key="index" :label="item.title" :value="item.field">
<span style="float: left">{{ item.field }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.title }}</span>
</el-option>
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import * as api from '../../api';
import { ref, reactive, onMounted } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { successMessage } from '/@/utils/message';
let formObj: any = reactive({
table: null,
primarykey: null,
field: null,
searchField: null,
oldSearchField: null,
});
let searchField = ref('');
let tableOptions: any = ref([]);
let labelOptions: any = ref([]);
const associationRef: any = ref<FormInstance>();
const emits = defineEmits(['updateVal']);
const props = defineProps(['value']);
// 初始化数据
const init = () => {
api.GetAssociationTable().then((res: any) => {
const { data } = res.data;
tableOptions = data;
// 设置默认选中
formObj.table = data[0].table;
labelOptions = data[0].tableFields;
formObj.primarykey = 'id';
formObj.field = 'id';
});
};
// 选中事件
const handleChange = (val: any) => {
const { tableFields } = tableOptions.find((item: any) => {
return item.table === val;
});
labelOptions = tableFields;
};
// 过滤条件选中
const handleSearch = (val: any) => {
const fields = labelOptions.filter((item: any) => {
return val.indexOf(item.field) > -1;
});
formObj.searchField = fields;
};
// 更新数据
const handleUpdate = () => {
emits('updateVal', formObj);
};
// 数据验证
const onSubmit = () => {
let res = false;
associationRef.value.validate((valid: any) => {
if (valid) {
res = true;
} else {
return false;
}
});
return res;
};
</script>
<style></style>

View File

@@ -0,0 +1,509 @@
<template>
<div>
<el-row :gutter="20">
<el-col :span="4">变量标题</el-col>
<el-col :span="4">变量名</el-col>
<el-col :span="10">变量值</el-col>
<el-col :span="2" :offset="1">是否前端配置</el-col>
<el-col :span="3" >操作</el-col>
</el-row>
<el-form ref="formRef" :model="formData" label-width="0px" label-position="left" style="margin-top: 20px">
<el-form-item
:prop="['array'].indexOf(item.form_item_type_label) > -1 ? '' : item.key"
:key="index"
:rules="item.rule || []"
v-for="(item, index) in formList"
>
<el-col :span="4">
<el-input v-if="item.edit" v-model="item.title" style="display: inline-block; width: 200px" placeholder="请输入标题"></el-input>
<span v-else>{{ item.title }}</span>
</el-col>
<el-col :span="4" >
<el-input v-if="item.edit" v-model="item.new_key" style="width: 200px" placeholder="请输入变量key">
<template slot="prepend">
<span style="padding: 0px 5px">{{ editableTabsItem.key }}</span>
</template>
</el-input>
<span v-else>{{ editableTabsItem.key }}.{{ item.key }}</span>
</el-col>
<el-col :span="10">
<!-- 文本 -->
<el-input
:key="index"
v-if="['text', 'textarea'].indexOf(item.form_item_type_label) > -1"
:type="item.form_item_type_label"
v-model="formData[item.key]"
:placeholder="item.placeholder"
clearable
></el-input>
<el-input-number :key="index + 1" v-else-if="item.form_item_type_label === 'number'" v-model="formData[item.key]" :min="0"></el-input-number>
<!-- datetimedatetime -->
<el-date-picker
v-else-if="['datetime', 'date', 'time'].indexOf(item.form_item_type_label) > -1"
v-model="formData[item.key]"
:key="index + 2"
:type="item.form_item_type_label"
:placeholder="item.placeholder"
>
</el-date-picker>
<!-- select -->
<el-select
:key="index + 3"
v-else-if="item.form_item_type_label === 'select'"
v-model="formData[item.key]"
:placeholder="item.placeholder"
clearable
>
<el-option v-for="item in dictionary(item.setting) || []" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
<!-- checkbox -->
<el-checkbox-group
:key="index + 4"
v-else-if="item.form_item_type_label === 'checkbox'"
v-model="formData[item.key]"
:placeholder="item.placeholder"
>
<el-checkbox v-for="item in dictionary(item.setting) || []" :key="item.value" :label="item.value" :value="item.value">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
<!-- radio -->
<el-radio-group
:key="index + 5"
v-else-if="item.form_item_type_label === 'radio'"
v-model="formData[item.key]"
:placeholder="item.placeholder"
clearable
>
<el-radio v-for="item in dictionary(item.setting) || []" :key="item.value" :label="item.value" :value="item.value">
{{ item.label }}
</el-radio>
</el-radio-group>
<!-- switch -->
<el-switch
:key="index + 6"
v-else-if="item.form_item_type_label === 'switch'"
v-model="formData[item.key]"
:inactive-value="false"
active-color="#13ce66"
inactive-color="#ff4949"
>
</el-switch>
<!-- 图片 -->
<div v-else-if="['img', 'imgs'].indexOf(item.form_item_type_label) > -1" :key="index + 7">
<el-upload
:action="uploadUrl"
:headers="uploadHeaders"
name="file"
:accept="'image/*'"
:on-preview="handlePictureCardPreview"
:on-success="
(response:any, file:any, fileList:any) => {
handleUploadSuccess(response, file, fileList, item.key);
}
"
:on-error="handleError"
:on-exceed="handleExceed"
:before-remove="
(file:any, fileList:any) => {
beforeRemove(file, fileList, item.key);
}
"
:multiple="item.form_item_type_label !== 'img'"
:limit="item.form_item_type_label === 'img' ? 1 : 5"
:ref="'imgUpload_' + item.key"
:data-keyname="item.key"
:file-list="item.value ? item.value : []"
list-type="picture-card"
>
<i class="el-icon-plus"></i>
<div slot="tip" class="el-upload__tip">选取图片后,需手动上传到服务器,并且只能上传jpg/png文件</div>
</el-upload>
<el-dialog :visible.sync="dialogImgVisible">
<img width="100%" :src="dialogImageUrl" alt="" />
</el-dialog>
</div>
<!-- 文件 -->
<div v-else-if="['file'].indexOf(item.form_item_type_label) > -1" :key="index + 8">
<el-upload
:action="uploadUrl"
:headers="uploadHeaders"
name="file"
:on-preview="handlePictureCardPreview"
:on-success="
(response:any, file:any, fileList:any) => {
handleUploadSuccess(response, file, fileList, item.key);
}
"
:on-error="handleError"
:on-exceed="handleExceed"
:before-remove="
(file:any, fileList:any) => {
beforeRemove(file, fileList, item.key);
}
"
:limit="5"
:ref="'fileUpload_' + item.key"
:data-keyname="item.key"
:file-list="item.value"
list-type="picture-card"
>
<i class="el-icon-plus"></i>
<div slot="tip" class="el-upload__tip">选取图片后,需手动上传到服务器,并且只能上传jpg/png文件</div>
</el-upload>
<el-dialog :visible.sync="dialogImgVisible">
<img width="100%" :src="dialogImageUrl" alt="" />
</el-dialog>
</div>
<!-- 关联表 -->
<div v-else-if="['foreignkey', 'manytomany'].indexOf(item.form_item_type_label) > -1" :key="index + 9">
<table-selector
v-model="formData[item.key]"
:el-props="{
pagination: true,
columns: item.setting.searchField,
}"
:dict="{
url: '/api/system/system_config/get_table_data/' + item.id + '/',
value: item.setting.primarykey,
label: item.setting.field,
}"
:pagination="true"
:multiple="item.form_item_type_label === 'manytomany'"
></table-selector>
</div>
<!-- 数组 -->
<div v-else-if="item.form_item_type_label === 'array'" :key="index + 10">
<vxe-table
border
resizable
auto-resize
show-overflow
keep-source
:ref="'xTable_' + item.key"
height="200"
:edit-rules="validRules"
:edit-config="{ trigger: 'click', mode: 'row', showStatus: true }"
>
<vxe-column field="title" title="标题" :edit-render="{ autofocus: '.vxe-input--inner' }">
<template #edit="{ row }">
<vxe-input v-model="row.title" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column field="key" title="键名" :edit-render="{ autofocus: '.vxe-input--inner' }">
<template #edit="{ row }">
<vxe-input v-model="row.key" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column field="value" title="键值" :edit-render="{}">
<template #edit="{ row }">
<vxe-input v-model="row.value" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column title="操作" width="100" show-overflow>
<template #default="{ row, index }">
<el-popover placement="top" width="160" v-model="childRemoveVisible">
<p>删除后无法恢复,确定删除吗?</p>
<div style="text-align: right; margin: 0">
<el-button size="mini" type="text" @click="childRemoveVisible = false">取消</el-button>
<el-button type="primary" size="mini" @click="onRemoveChild(row, index, item.key)">确定</el-button>
</div>
<el-button type="text" slot="reference">删除</el-button>
</el-popover>
</template>
</vxe-column>
</vxe-table>
<div>
<el-button size="mini" @click="onAppend('xTable_' + item.key)">追加</el-button>
</div>
</div>
</el-col>
<el-col :span="2" :offset="1">
<el-switch v-model="item.status" active-color="#13ce66" inactive-color="#ff4949"> </el-switch>
</el-col>
<el-col :span="3">
<el-button v-if="item.edit" size="mini" type="primary" :icon="Finished" @click="onEditSave(item)">保存</el-button>
<el-button v-else size="mini" type="primary" :icon="Edit" @click="onEdit(index)"></el-button>
<el-popconfirm title="确定删除该条数据吗" @confirm="onDelRow(item)">
<template #reference>
<el-button size="mini" type="danger" :icon="Delete" ></el-button>
</template>
</el-popconfirm>
</el-col>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit(formRef)">确定</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import * as api from '../api';
import { dictionary } from '/@/utils/dictionary';
import { getBaseURL } from '/@/utils/baseUrl';
import { ref, reactive, watch, nextTick,inject } from 'vue';
import type { FormInstance, FormRules, TableInstance } from 'element-plus';
import { successMessage, errorMessage } from '/@/utils/message';
import { Session } from '/@/utils/storage';
import {Edit,Finished,Delete} from "@element-plus/icons-vue";
const props = defineProps(['options', 'editableTabsItem']);
let formData: any = reactive({});
let formList: any = ref([]);
let childTableData = ref([]);
let childRemoveVisible = ref(false);
const validRules = reactive<FormRules>({
title: [
{
required: true,
message: '必须填写',
},
],
key: [
{
required: true,
message: '必须填写',
},
],
value: [
{
required: true,
message: '必须填写',
},
],
});
const formRef = ref<FormInstance>()
let uploadUrl = ref(getBaseURL() + 'api/system/file/');
let uploadHeaders = ref({
Authorization: 'JWT ' + Session.get('token'),
});
let dialogImageUrl = ref('');
let dialogImgVisible = ref(false);
let uploadImgKey = ref(null);
// 获取数据
const getInit = () => {
api.GetList({ parent: props.options.id, limit: 999 }).then((res: any) => {
let data = res.data;
formList.value = data;
const _formData: any = {};
for (const item of data) {
const key = item.key;
if (item.value) {
_formData[key] = item.value;
} else {
if ([5, 12, 14].indexOf(item.form_item_type) !== -1) {
_formData[key] = [];
} else {
_formData[key] = item.value;
}
}
if (item.form_item_type_label === 'array') {
console.log('test');
nextTick(() => {
const tableName = 'xTable_' + key;
const tabelRef = ref<TableInstance>();
console.log(tabelRef);
// const $table = this.$refs[tableName][0];
// $table.loadData(item.chinldern);
});
}
}
formData = Object.assign(formData, _formData)
});
};
// 提交数据
const onSubmit = (formEl: FormInstance | undefined) => {
// const form = JSON.parse(JSON.stringify(form));
const keys = Object.keys(formData);
const values = Object.values(formData);
for (const index in formList.value) {
const item = formList.value[index];
// eslint-disable-next-line camelcase
const form_item_type_label = item.form_item_type_label;
// eslint-disable-next-line camelcase
if (form_item_type_label === 'array') {
const parentId = item.id;
const tableName = 'xTable_' + item.key;
// const $table = this.$refs[tableName][0];
// const { tableData } = $table.getTableData();
// for (const child of tableData) {
// if (!child.id && child.key && child.value) {
// child.parent = parentId;
// child.id = null;
// formList.push(child);
// }
// }
// // 必填项的判断
// for (const arr of item.rule) {
// if (arr.required && tableData.length === 0) {
// errorMessage(item.title + '不能为空');
// return;
// }
// }
// item.value = tableData;
}
// 赋值操作
keys.map((mapKey, mapIndex) => {
if (mapKey === item.key) {
if (item.form_item_type_label !== 'array') {
item.value = values[mapIndex];
}
// 必填项的验证
if (['img', 'imgs'].indexOf(item.form_item_type_label) > -1) {
for (const arr of item.rule) {
if (arr.required && item.value === null) {
errorMessage(item.title + '不能为空');
return;
}
}
}
}
});
}
// formRef.value.clearValidate();
if (!formEl) return
formEl.validate((valid:any) => {
if (valid) {
api.saveContent(formList.value).then((res:any) => {
successMessage('保存成功');
refreshView&&refreshView();
});
} else {
console.log('error submit!!');
return false;
}
});
};
// 追加
const onAppend = (tableName: any) => {
// const $table = this.$refs[tableName][0];
// const { tableData } = $table.getTableData();
// const tableLength = tableData.length;
// if (tableLength === 0) {
// const { row: newRow } = $table.insert();
// console.log(newRow);
// } else {
// const errMap = $table.validate().catch((errMap: any) => errMap);
// if (errMap) {
// errorMessage('校验不通过!');
// } else {
// const { row: newRow } = $table.insert();
// console.log(newRow);
// }
// }
};
// 子表删除
const onRemoveChild = (row: any, index: any, refName: any) => {
console.log(row, index);
if (row.id) {
api.DelObj(row.id).then((res: any) => {
// this.refreshView();
});
} else {
// this.childTableData.splice(index, 1);
// const tableName = 'xTable_' + refName;
// const tableData = this.$refs[tableName][0].remove(row);
// console.log(tableData);
}
};
// 图片预览
const handlePictureCardPreview = (file: any) => {
dialogImageUrl = file.url;
dialogImgVisible.value = true;
};
// 判断是否为图片
// 封装一个判断图片文件后缀名的方法
const isImage = (fileName: any) => {
if (typeof fileName !== 'string') return;
const name = fileName.toLowerCase();
return name.endsWith('.png') || name.endsWith('.jpeg') || name.endsWith('.jpg') || name.endsWith('.png') || name.endsWith('.bmp');
};
// 上传成功
const handleUploadSuccess = (response: any, file: any, fileList: any, imgKey: any) => {
const that = this;
const { code, msg } = response;
if (code === 2000) {
const { url } = response.data;
const { name } = file;
const type = isImage(name);
if (!type) {
errorMessage('只允许上传图片');
} else {
const uploadImgKey = formData[imgKey];
if (!uploadImgKey || uploadImgKey === '') {
formData[imgKey] = [];
}
// console.log(len)
const dict = {
name: name,
url: getBaseURL() + url,
};
formData[imgKey].push(dict);
}
} else {
errorMessage('上传失败,' + JSON.stringify(msg));
}
};
// 上传失败
const handleError = () => {
errorMessage('上传失败');
};
// 上传超出限制
const handleExceed = () => {
errorMessage('超过文件上传数量');
};
// 删除时的钩子
const beforeRemove = (file: any, fileList: any, key: any) => {
var index = 0;
formData[key].map((value: any, inx: any) => {
if (value.uid === file.uid) index = inx;
});
formData[key].splice(index, 1);
};
// 配置的行删除
const onDelRow = (obj: any) => {
api.DelObj(obj.id).then((res: any) => {
// this.refreshView();
});
};
// 行编辑
const onEdit = (index: any) => {
formList.value[index].edit =true
formList.value[index].new_key =formList.value[index].key
};
// 行编辑保存
const refreshView = inject<Function>('refreshView')
const onEditSave = (obj: any) => {
obj.key = JSON.parse(JSON.stringify(obj.new_key));
api.UpdateObj(obj).then((res: any) => {
refreshView && refreshView()
});
};
watch(
props.options,
(nv) => {
if (nv && nv.id) {
getInit();
}
},
{ immediate: true }
);
</script>
<style></style>

View File

@@ -0,0 +1,82 @@
<template>
<el-card>
<div>
<el-header>
<div class="yxt-flex-between">
<div>
<el-tag>系统配置:您可以对您的网站进行自定义配置</el-tag>
</div>
<div>
<el-button-group>
<el-button type="primary" size="small" :icon="FolderAdd" @click="tabsDrawer = true"> 添加分组 </el-button>
<el-button size="small" type="warning" :icon="Edit" @click="contentDrawer = true"> 添加内容 </el-button>
</el-button-group>
</div>
</div>
</el-header>
</div>
<div>
<el-drawer v-if="tabsDrawer" title="添加分组" v-model="tabsDrawer" direction="rtl" size="30%">
<addTabs></addTabs>
</el-drawer>
</div>
<div>
<el-drawer v-if="contentDrawer" title="添加内容" v-model="contentDrawer" direction="rtl" size="30%">
<addContent></addContent>
</el-drawer>
</div>
<el-tabs type="border-card" v-model="editableTabsValue">
<el-tab-pane :key="index" v-for="(item, index) in editableTabs" :label="item.title" :name="item.key">
<span slot="label" v-if="item.icon"><i :class="item.icon" style="font-weight: 1000; font-size: 16px"></i></span>
<el-row v-if="item.icon">
<el-col :offset="4" :span="8">
<addContent></addContent>
</el-col>
</el-row>
<formContent v-else :options="item" :editableTabsItem="item"></formContent>
</el-tab-pane>
</el-tabs>
</el-card>
</template>
<script lang="ts" setup name="config">
import { Edit, FolderAdd } from '@element-plus/icons-vue';
import * as api from './api';
import addTabs from './components/addTabs.vue';
import addContent from './components/addContent.vue';
import formContent from './components/formContent.vue';
import { ref, onMounted } from 'vue';
let tabsDrawer = ref(false);
let contentDrawer = ref(false);
let editableTabsValue = ref('base');
let editableTabs: any = ref([]);
const getTabs = () => {
api
.GetList({
limit: 999,
parent__isnull: true,
})
.then((res: any) => {
let data = res.data;
data.push({
title: '无',
icon: 'el-icon-plus',
key: 'null',
});
editableTabs.value = data;
});
};
onMounted(() => {
getTabs();
});
</script>
<style>
/*用 flex 两边对齐*/
.yxt-flex-between {
display: flex;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,49 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/dept/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(obj: DelReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'delete',
data: obj,
});
}
export function lazyLoadDept(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}

View File

@@ -0,0 +1,251 @@
import * as api from './api';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { verifyPhone } from '/@/utils/toolsValidate';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '/@/utils/message';
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
const validatePhone = async (rule: any, value: any, callback: any) => {
if (value === '') {
throw new Error('请输入手机号码');
}
if (verifyPhone(value)) {
callback();
} else {
throw new Error('手机号码格式有误');
}
};
/**
* 懒加载
* @param row
* @returns {Promise<unknown>}
*/
const loadContentMethod = (tree: any, treeNode: any, resolve: any) => {
api.GetList({ parent: tree.id }).then((res: any) => {
resolve(res.data);
});
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
pagination: {
show: false,
},
table: {
rowKey: 'id',
lazy: true,
load: loadContentMethod,
treeProps: { children: 'children', hasChildren: 'hasChild' },
},
rowHandle: {
fiexd: 'right',
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
addChildren: {
text: '添加子级',
type: 'text',
click(context) {
const rowId = context.row.id;
crudExpose!.openAdd({ row: { parent: rowId } });
},
},
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
search: {
title: '关键词',
column: {
show: false,
},
search: {
show: true,
component: {
props: {
clearable: true,
},
placeholder: '请输入关键词',
},
},
form: {
show: false,
component: {
props: {
clearable: true,
},
},
},
},
name: {
title: '部门名称',
sortable: true,
treeNode: true, // 设置为树形列
search: {
disabled: false,
component: {
props: {
clearable: true,
},
},
},
width: 180,
type: 'input',
form: {
rules: [
// 表单校验规则
{ required: true, message: '部门名称必填项' },
],
component: {
span: 12,
props: {
clearable: true,
},
placeholder: '请输入部门名称',
},
},
},
key: {
title: '部门标识',
sortable: true,
form: {
component: {
props: {
clearable: true,
},
placeholder: '请输入标识字符',
},
},
},
owner: {
title: '负责人',
sortable: true,
form: {
component: {
span: 12,
props: {
clearable: true,
},
placeholder: '请输入负责人',
},
},
},
phone: {
title: '联系电话',
sortable: true,
form: {
rules: [{ validator: validatePhone, trigger: 'blur' }],
component: {
span: 12,
props: {
clearable: true,
},
placeholder: '请输入联系电话',
},
},
},
email: {
title: '邮箱',
sortable: true,
form: {
component: {
span: 12,
props: {
clearable: true,
},
placeholder: '请输入邮箱',
},
rules: [
{
type: 'email',
message: '请输入正确的邮箱地址',
// @ts-ignore
trigger: ['blur', 'change'],
},
],
},
},
sort: {
title: '排序',
sortable: true,
width: 80,
type: 'number',
form: {
value: 1,
component: {
span: 12,
placeholder: '请选择序号',
},
},
},
status: {
title: '状态',
sortable: true,
search: {
disabled: false,
},
type: 'dict-radio',
column: {
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: #409eff; --el-switch-off-color: #dcdfe6',
onChange: compute((context) => {
return () => {
api.UpdateObj(context.row).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
};
}),
},
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
},
},
};
};

View File

@@ -0,0 +1,308 @@
<template>
<fs-page>
<splitpanes>
<pane max-size="20">
<el-card :body-style="{ height: '100%' }">
<p class="font-mono font-black text-center text-xl pb-5">部门列表</p>
<el-input v-model="filterText" :placeholder="placeholder"/>
<el-tree
ref="treeRef"
class="font-mono font-bold leading-6 text-7xl"
:data="data"
:props="treeProps"
:filter-node-method="filterNode"
:load="loadNode"
@node-drop="nodeDrop"
lazy
icon="ArrowRightBold"
:indent="12"
draggable
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span v-if="data.status" class="text-center font-black font-normal"><SvgIcon
:name="node.data.icon"/>&nbsp;{{ node.label }}</span>
<span v-else class="text-center font-black font-normal text-red-700"><SvgIcon
:name="node.data.icon"/>&nbsp;{{ node.label }}</span>
</template>
</el-tree>
</el-card>
</pane>
<pane>
<el-card :body-style="{ height: '100%' }">
<el-form ref="formRef" :rules="rules" :model="form" label-width="120px" label-position="right">
<el-divider>
<strong>部门配置</strong>
</el-divider>
<el-row>
<el-col :span="10">
<el-form-item label="部门ID" prop="id">
<el-input v-model="form.id" disabled/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="父级部门ID" prop="parent">
<el-input v-model="form.parent"/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item required label="部门名称" prop="name">
<el-input v-model="form.name"/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item required label="部门标识" prop="key">
<el-input v-model="form.key"/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="负责人" prop="owner">
<el-input v-model="form.owner"/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone"/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email"/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" controls-position="right"/>
</el-form-item>
</el-col>
<el-col class="center">
<el-divider>
<el-button @click="saveMenu()" type="primary" round>保存</el-button>
<el-button @click="newMenu()" type="success" round :disabled="!form.id">新建
</el-button>
<el-button @click="addChildMenu()" type="info" round :disabled="!form.id">添加子级
</el-button>
<el-button @click="addSameLevelMenu()" type="warning" round :disabled="!form.id">
添加同级
</el-button>
<el-button @click="deleteMenu()" type="danger" round :disabled="!form.id">删除部门
</el-button>
</el-divider>
</el-col>
</el-row>
</el-form>
</el-card>
</pane>
</splitpanes>
</fs-page>
</template>
<script lang="ts" setup name="dept">
import {Splitpanes, Pane} from 'splitpanes';
import 'splitpanes/dist/splitpanes.css';
import * as api from './api';
import {ElForm, ElMessageBox, ElTree, FormRules} from 'element-plus';
import {ref, onMounted, reactive, toRaw, watch} from 'vue';
import XEUtils from 'xe-utils';
import {errorMessage, successMessage} from '../../../utils/message';
interface Tree {
id: number;
name: string;
status: boolean;
children?: Tree[];
}
interface APIResponseData {
code?: number;
data: [];
msg?: string;
}
interface Form<T> {
[key: string]: T;
}
const placeholder = ref('请输入部门名称');
const filterText = ref('');
const treeRef = ref<InstanceType<typeof ElTree>>();
const treeProps = {
children: 'children',
label: 'name',
// isLeaf: (data: Tree[], node: Node) => {
// // @ts-ignore
// if (node.data.is_catalog) {
// return false;
// } else {
// return true;
// }
// },
};
const validateWebPath = (rule: string, value: string, callback: Function) => {
let pattern = /^\/.*?/;
if (!pattern.test(value)) {
callback(new Error('请输入正确的地址'));
} else {
callback();
}
};
watch(filterText, (val) => {
treeRef.value!.filter(val);
});
const filterNode = (value: string, data: Tree) => {
if (!value) return true;
return toRaw(data).name.indexOf(value) !== -1;
};
// 懒加载
const loadNode = (node: Node, resolve: (data: Tree[]) => void) => {
// @ts-ignore
if (node.level !== 0) {
// @ts-ignore
api.lazyLoadDept({parent: node.data.id}).then((res: APIResponseData) => {
resolve(res.data);
});
}
};
const nodeDrop = (draggingNode: Node, dropNode: Node, dropType: string, event: any) => {
// @ts-ignore
if (!dropNode.isLeaf) {
// @ts-ignore
api.dragMenu({menu_id: draggingNode.data.id, parent_id: dropNode.data.id}).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
}
};
let data = ref([]);
let isAddNewMenu = ref(false); // 判断当前是新增部门,还是更新保存当前部门
let form: Form<any> = reactive({
id: '',
parent: '',
name: '',
owner: '',
phone: '',
email: '',
sort: '',
});
const formRef = ref<InstanceType<typeof ElForm>>();
const rules = reactive<FormRules>({
// @ts-ignore
name: [{required: true, message: '部门名称必填', trigger: 'blur'}],
key: [{required: true, message: '部门标识必填', trigger: 'blur'}],
});
const getData = () => {
api.GetList({}).then((ret: APIResponseData) => {
const responseData = ret.data;
const result = XEUtils.toArrayTree(responseData, {
parentKey: 'parent',
children: 'children',
strict: true,
});
data.value = result;
});
};
const saveMenu = () => {
formRef.value?.validate((valid, fields) => {
if (valid) {
if (!isAddNewMenu.value) {
// 保存部门
form.component == '' ? (form.is_catalog = true) : (form.is_catalog = false);
api.UpdateObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
} else {
// 新增部门
form.component == '' ? (form.is_catalog = true) : (form.is_catalog = false);
api.AddObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
}
} else {
errorMessage('请填写检查表单');
}
});
};
const newMenu = () => {
formRef.value?.resetFields();
isAddNewMenu.value = true;
};
const addChildMenu = () => {
let parentId = form.id;
formRef.value?.resetFields();
form.parent = parentId;
isAddNewMenu.value = true;
};
const addSameLevelMenu = () => {
let parentId = form.parent;
formRef.value?.resetFields();
form.parent = parentId;
isAddNewMenu.value = true;
};
const deleteMenu = () => {
ElMessageBox.confirm(
'您确认删除该菜单项吗?',
'温馨提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
api.DelObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
})
};
const handleNodeClick = (data: any, node: any, prop: any) => {
Object.keys(toRaw(data)).forEach((key: string) => {
form[key] = data[key];
});
delete form.component_name;
form.id = data.id;
isAddNewMenu.value = false;
};
// 页面打开后获取列表数据
onMounted(() => {
getData();
});
</script>
<style lang="scss" scoped>
.el-row {
height: 100%;
.el-col {
height: 100%;
}
}
.el-card {
height: 100%;
}
.font-normal {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
}
</style>

View File

@@ -0,0 +1,42 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import XEUtils from 'xe-utils';
export const apiPrefix = '/api/system/dictionary/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
})
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,209 @@
import * as api from './api';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { dictionary } from '/@/utils/dictionary';
import { inject, nextTick, ref } from 'vue';
import { successMessage } from '/@/utils/message';
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
//权限判定
const hasPermissions = inject('$hasPermissions');
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show: hasPermissions('dictionary:Update'),
},
remove: {
iconRight: 'Delete',
type: 'text',
show: hasPermissions('dictionary:Delete'),
},
custom: {
text: '字典配置',
type: 'text',
show: hasPermissions('dictionary:Update'),
tooltip: {
placement: 'top',
content: '字典配置',
},
//@ts-ignore
click: (ctx: any) => {
const { row } = ctx;
context!.subDictRef.value.drawer = true;
nextTick(() => {
context!.subDictRef.value.setSearchFormData({ form: { parent: row.id } });
context!.subDictRef.value.doRefresh();
});
},
},
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose!.crudBinding.value.pagination;
// @ts-ignore
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
search: {
title: '关键词',
column: {
show: false,
},
search: {
show: true,
component: {
props: {
clearable: true,
},
placeholder: '请输入关键词',
},
},
form: {
show: false,
component: {
props: {
clearable: true,
},
},
},
},
label: {
title: '字典名称',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
type: 'input',
column: {
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '字典名称必填项' },
],
component: {
props: {
clearable: true,
},
placeholder: '请输入字典名称',
},
},
},
value: {
title: '字典编号',
search: {
disabled: true,
component: {
props: {
clearable: true,
},
},
},
type: 'input',
column: {
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{ required: true, message: '字典编号必填项' },
],
component: {
props: {
clearable: true,
},
placeholder: '请输入字典编号',
},
helper: {
render(h) {
return <el-alert title="使用方法dictionary('字典编号')" type="warning" />;
},
},
},
},
status: {
title: '状态',
search: {
show: true,
},
type: 'dict-radio',
column: {
minWidth: 90,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
onChange: compute((context) => {
return () => {
api.UpdateObj(context.row).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
};
}),
},
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
sort: {
title: '排序',
type: 'number',
column: {
minWidth: 80,
},
form: {
value: 1,
},
},
},
},
};
};

View File

@@ -0,0 +1,21 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
<subDict ref="subDictRef"></subDict>
</fs-page>
</template>
<script lang="ts" setup name="dictionary">
import { ref, onMounted, defineAsyncComponent } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
const subDict = defineAsyncComponent(() => import('./subDict/index.vue'));
const subDictRef = ref();
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: { subDictRef } });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>

View File

@@ -0,0 +1,41 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/dictionary/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id+'/',
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { },
});
}

View File

@@ -0,0 +1,309 @@
import * as api from './api';
import {
dict,
UserPageQuery,
AddReq,
DelReq,
EditReq,
CrudOptions,
CreateCrudOptionsProps,
CreateCrudOptionsRet
} from '@fast-crud/fast-crud';
import {dictionary} from '/@/utils/dictionary';
export const createCrudOptions = function ({crudExpose, context}: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({form, row}: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({row}: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({form}: AddReq) => {
const data = crudExpose!.getSearchFormData()
const parent = data.parent
form.parent = parent
if (parent) {
return await api.AddObj(form);
} else {
return undefined
}
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
},
columns: {
_index: {
title: '序号',
form: {show: false},
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose!.crudBinding.value.pagination;
// @ts-ignore
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
label: {
title: '名称',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
type: 'input',
form: {
rules: [
// 表单校验规则
{required: true, message: '名称必填项'},
],
component: {
props: {
clearable: true,
},
placeholder: '请输入名称',
},
},
},
type: {
title: '数据值类型',
type: 'dict-select',
search: {
disabled: true,
component: {
props: {
clearable: true,
},
},
},
show: false,
dict: dict({
data: [
{label: 'text', value: 0},
{label: 'number', value: 1},
{label: 'date', value: 2},
{label: 'datetime', value: 3},
{label: 'time', value: 4},
{label: 'file', value: 5},
{label: 'boolean', value: 6},
{label: 'images', value: 7},
],
}),
form: {
rules: [
// 表单校验规则
{required: true, message: '数据值类型必填项'},
],
value: 0,
component: {
props: {
clearable: true,
},
placeholder: '请选择数据值类型',
},
/* valueChange(key, value, form, { getColumn, mode, component, immediate, getComponent }) {
const template = vm.getEditFormTemplate('value')
// 选择框重新选择后情况value值
if (!immediate) {
form.value = undefined
}
if (value === 0) {
template.component.name = 'el-input'
} else if (value === 1) {
template.component.name = 'el-input-number'
} else if (value === 2) {
template.component.name = 'el-date-picker'
template.component.props = {
type: 'date',
valueFormat: 'yyyy-MM-dd'
}
} else if (value === 3) {
template.component.name = 'el-date-picker'
template.component.props = {
type: 'datetime',
valueFormat: 'yyyy-MM-dd HH:mm:ss'
}
} else if (value === 4) {
template.component.name = 'el-time-picker'
template.component.props = {
pickerOptions: {
arrowControl: true
},
valueFormat: 'HH:mm:ss'
}
} else if (value === 5) {
template.component.name = 'd2p-file-uploader'
template.component.props = { elProps: { listType: 'text' } }
} else if (value === 6) {
template.component.name = 'dict-switch'
template.component.value = true
template.component.props = {
dict: {
data: [
{ label: '是', value: 'true' },
{ label: '否', value: 'false' }
]
}
}
} else if (value === 7) {
template.component.name = 'd2p-cropper-uploader'
template.component.props = { accept: '.png,.jpeg,.jpg,.ico,.bmp,.gif', cropper: { viewMode: 1 } }
}
}, */
},
},
value: {
title: '数据值',
search: {
show: true,
component: {
props: {
clearable: true,
},
},
},
view: {
component: {props: {height: 100, width: 100}},
},
/* // 提交时,处理数据
valueResolve(row: any, col: any) {
const value = row[col.key]
const type = row.type
if (type === 5 || type === 7) {
if (value != null) {
if (value.length >= 0) {
if (value instanceof Array) {
row[col.key] = value.toString()
} else {
row[col.key] = value
}
} else {
row[col.key] = null
}
}
} else {
row[col.key] = value
}
},
// 接收时,处理数据
valueBuilder(row: any, col: any) {
const value = row[col.key]
const type = row.type
if (type === 5 || type === 7) {
if (value != null && value) {
row[col.key] = value.split(',')
}
} else {
row[col.key] = value
}
}, */
type: 'input',
form: {
rules: [
// 表单校验规则
{required: true, message: '数据值必填项'},
],
component: {
props: {
clearable: true,
},
placeholder: '请输入数据值',
},
},
},
status: {
title: '状态',
width: 80,
search: {
show: true
},
type: 'dict-radio',
dict: dict({
data: dictionary('button_status_bool'),
}),
form: {
value: true,
rules: [
// 表单校验规则
{required: true, message: '状态必填项'},
],
},
},
sort: {
title: '排序',
width: 70,
type: 'number',
form: {
value: 1,
component: {},
rules: [
// 表单校验规则
{required: true, message: '排序必填项'},
],
},
},
color: {
title: '标签颜色',
width: 90,
search: {
disabled: true,
},
type: 'dict-select',
dict: dict({
data: [
{label: 'success', value: 'success', color: 'success'},
{label: 'primary', value: 'primary', color: 'primary'},
{label: 'info', value: 'info', color: 'info'},
{label: 'danger', value: 'danger', color: 'danger'},
{label: 'warning', value: 'warning', color: 'warning'},
],
}),
form: {
component: {
props: {
clearable: true,
},
},
},
},
},
},
};
};

View File

@@ -0,0 +1,42 @@
<template>
<el-drawer size="70%" v-model="drawer" direction="rtl" destroy-on-close :before-close="handleClose">
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</el-drawer>
</template>
<script lang="ts" setup>
import { ref, onMounted, defineAsyncComponent } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { ElMessageBox } from 'element-plus';
//抽屉是否显示
const drawer = ref(false);
//抽屉关闭确认
const handleClose = (done: () => void) => {
ElMessageBox.confirm('您确定要关闭?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
done();
})
.catch(() => {
// catch error
});
};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
const { setSearchFormData, doRefresh } = crudExpose;
defineExpose({ drawer, setSearchFormData, doRefresh });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>

View File

@@ -0,0 +1,118 @@
<template>
<div class="error layout-view-bg-white" :style="{ height: `calc(100vh - ${initTagViewHeight}` }">
<div class="error-flex">
<div class="left">
<div class="left-item">
<div class="left-item-animation left-item-num">401</div>
<div class="left-item-animation left-item-title">{{ $t('message.noAccess.accessTitle') }}</div>
<div class="left-item-animation left-item-msg">{{ $t('message.noAccess.accessMsg') }}</div>
<div class="left-item-animation left-item-btn">
<el-button type="primary" round @click="onSetAuth">{{ $t('message.noAccess.accessBtn') }}</el-button>
</div>
</div>
</div>
<div class="right">
<img
src="https://img-blog.csdnimg.cn/3333f265772a4fa89287993500ecbf96.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbHl0LXRvcA==,size_16,color_FFFFFF,t_70,g_se,x_16"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { Session } from '/@/utils/storage';
export default defineComponent({
name: '401',
setup() {
const storesThemeConfig = useThemeConfig();
const storesTagsViewRoutes = useTagsViewRoutes();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
const onSetAuth = () => {
// https://gitee.com/lyt-top/vue-next-admin/issues/I5C3JS
// 清除缓存/token等
Session.clear();
// 使用 reload 时,不需要调用 resetRoute() 重置路由
window.location.reload();
};
// 设置主内容的高度
const initTagViewHeight = computed(() => {
let { isTagsview } = themeConfig.value;
if (isTagsViewCurrenFull.value) {
return `30px`;
} else {
if (isTagsview) return `114px`;
else return `80px`;
}
});
return {
onSetAuth,
initTagViewHeight,
};
},
});
</script>
<style scoped lang="scss">
.error {
height: 100%;
background-color: var(--el-color-white);
display: flex;
.error-flex {
margin: auto;
display: flex;
height: 350px;
width: 900px;
.left {
flex: 1;
height: 100%;
align-items: center;
display: flex;
.left-item {
.left-item-animation {
opacity: 0;
animation-name: error-num;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
.left-item-num {
color: var(--el-color-info);
font-size: 55px;
}
.left-item-title {
font-size: 20px;
color: var(--el-text-color-primary);
margin: 15px 0 5px 0;
animation-delay: 0.1s;
}
.left-item-msg {
color: var(--el-text-color-secondary);
font-size: 12px;
margin-bottom: 30px;
animation-delay: 0.2s;
}
.left-item-btn {
animation-delay: 0.2s;
}
}
}
.right {
flex: 1;
opacity: 0;
animation-name: error-img;
animation-duration: 2s;
animation-fill-mode: forwards;
img {
width: 100%;
height: 100%;
}
}
}
}
</style>

View File

@@ -0,0 +1,115 @@
<template>
<div class="error layout-view-bg-white" :style="{ height: `calc(100vh - ${initTagViewHeight}` }">
<div class="error-flex">
<div class="left">
<div class="left-item">
<div class="left-item-animation left-item-num">404</div>
<div class="left-item-animation left-item-title">{{ $t('message.notFound.foundTitle') }}</div>
<div class="left-item-animation left-item-msg">{{ $t('message.notFound.foundMsg') }}</div>
<div class="left-item-animation left-item-btn">
<el-button type="primary" round @click="onGoHome">{{ $t('message.notFound.foundBtn') }}</el-button>
</div>
</div>
</div>
<div class="right">
<img
src="https://img-blog.csdnimg.cn/9eb1d85a417f4ed1ba7107f149ce3da1.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbHl0LXRvcA==,size_16,color_FFFFFF,t_70,g_se,x_16"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
export default defineComponent({
name: '404',
setup() {
const storesThemeConfig = useThemeConfig();
const storesTagsViewRoutes = useTagsViewRoutes();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
const router = useRouter();
const onGoHome = () => {
router.push('/');
};
// 设置主内容的高度
const initTagViewHeight = computed(() => {
let { isTagsview } = themeConfig.value;
if (isTagsViewCurrenFull.value) {
return `30px`;
} else {
if (isTagsview) return `114px`;
else return `80px`;
}
});
return {
onGoHome,
initTagViewHeight,
};
},
});
</script>
<style scoped lang="scss">
.error {
height: 100%;
background-color: var(--el-color-white);
display: flex;
.error-flex {
margin: auto;
display: flex;
height: 350px;
width: 900px;
.left {
flex: 1;
height: 100%;
align-items: center;
display: flex;
.left-item {
.left-item-animation {
opacity: 0;
animation-name: error-num;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
.left-item-num {
color: var(--el-color-info);
font-size: 55px;
}
.left-item-title {
font-size: 20px;
color: var(--el-text-color-primary);
margin: 15px 0 5px 0;
animation-delay: 0.1s;
}
.left-item-msg {
color: var(--el-text-color-secondary);
font-size: 12px;
margin-bottom: 30px;
animation-delay: 0.2s;
}
.left-item-btn {
animation-delay: 0.2s;
}
}
}
.right {
flex: 1;
opacity: 0;
animation-name: error-img;
animation-duration: 2s;
animation-fill-mode: forwards;
img {
width: 100%;
height: 100%;
}
}
}
}
</style>

View File

@@ -0,0 +1,41 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/file/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,131 @@
import * as api from './api';
import { UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
return {
crudOptions: {
actionbar: {
buttons: {
add: {
show: false,
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose!.crudBinding.value.pagination;
return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1;
},
},
},
search: {
title: '关键词',
column: {
show: false,
},
search: {
show: true,
component: {
props: {
clearable: true,
},
placeholder: '请输入关键词',
},
},
form: {
show: false,
component: {
props: {
clearable: true,
},
},
},
},
name: {
title: '文件名称',
search: {
show: true,
},
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入文件名称',
},
},
},
url: {
title: '文件地址',
type: 'file-uploader',
search: {
disabled: true,
},
column:{
minWidth: 200,
},
},
md5sum: {
title: '文件MD5',
search: {
disabled: true,
},
column:{
minWidth: 120,
},
form: {
disabled: false,
},
},
},
},
};
};

View File

@@ -0,0 +1,26 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
// 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,41 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/login_log/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,331 @@
import * as api from './api';
import { UserPageQuery, AddReq, DelReq, EditReq, CreateCrudOptionsProps, CreateCrudOptionsRet, dict } from '@fast-crud/fast-crud';
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: false,
},
},
},
rowHandle: {
fixed:'right',
width: 100,
buttons: {
view: {
type: 'text',
},
edit: {
show: false,
},
remove: {
show: false,
},
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose!.crudBinding.value.pagination;
return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1;
},
},
},
search: {
title: '关键词',
column: {
show: false,
},
search: {
show: true,
component: {
props: {
clearable: true,
},
placeholder: '请输入关键词',
},
},
form: {
show: false,
component: {
props: {
clearable: true,
},
},
},
},
username: {
title: '登录用户名',
search: {
disabled: false,
},
type: 'input',
column:{
minWidth: 120,
},
form: {
disabled: true,
component: {
placeholder: '请输入登录用户名',
},
},
},
ip: {
title: '登录ip',
search: {
disabled: false,
},
type: 'input',
column:{
minWidth: 120,
},
form: {
disabled: true,
component: {
placeholder: '请输入登录ip',
},
},
},
isp: {
title: '运营商',
search: {
disabled: true,
},
disabled: true,
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入运营商',
},
},
},
continent: {
title: '大州',
type: 'input',
column:{
minWidth: 90,
},
form: {
disabled: true,
component: {
placeholder: '请输入大州',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
country: {
title: '国家',
type: 'input',
column:{
minWidth: 90,
},
form: {
component: {
placeholder: '请输入国家',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
province: {
title: '省份',
type: 'input',
column:{
minWidth: 80,
},
form: {
component: {
placeholder: '请输入省份',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
city: {
title: '城市',
type: 'input',
column:{
minWidth: 80,
},
form: {
component: {
placeholder: '请输入城市',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
district: {
title: '县区',
key: '',
type: 'input',
column:{
minWidth: 80,
},
form: {
component: {
placeholder: '请输入县区',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
area_code: {
title: '区域代码',
type: 'input',
column:{
minWidth: 90,
},
form: {
component: {
placeholder: '请输入区域代码',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
country_english: {
title: '英文全称',
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入英文全称',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
country_code: {
title: '简称',
type: 'input',
column:{
minWidth: 100,
},
form: {
component: {
placeholder: '请输入简称',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
longitude: {
title: '经度',
type: 'input',
disabled: true,
column:{
minWidth: 100,
},
form: {
component: {
placeholder: '请输入经度',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
latitude: {
title: '纬度',
type: 'input',
disabled: true,
column:{
minWidth: 100,
},
form: {
component: {
placeholder: '请输入纬度',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
login_type: {
title: '登录类型',
type: 'dict-select',
search: {
disabled: false,
},
dict: dict({
data: [
{ label: '普通登录', value: 1 },
{ label: '微信扫码登录', value: 2 },
],
}),
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请选择登录类型',
},
},
},
os: {
title: '操作系统',
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入操作系统',
},
},
},
browser: {
title: '浏览器名',
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入浏览器名',
},
},
},
agent: {
title: 'agent信息',
disabled: true,
type: 'input',
column:{
minWidth: 120,
},
form: {
component: {
placeholder: '请输入agent信息',
},
},
},
},
},
};
};

View File

@@ -0,0 +1,18 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts" setup name="loginLog">
import { ref, onMounted } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>

View File

@@ -0,0 +1,41 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/operation_log/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,254 @@
import * as api from './api';
import { UserPageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: false,
},
},
},
rowHandle: {
fixed:'right',
width: 100,
buttons: {
view: {
type: 'text',
},
edit: {
show: false,
},
remove: {
show: false,
},
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination = crudExpose!.crudBinding.value.pagination;
return ((pagination!.currentPage ?? 1) - 1) * pagination!.pageSize + index + 1;
},
},
},
search: {
title: '关键词',
column: {
show: false,
},
search: {
show: true,
component: {
props: {
clearable: true,
},
placeholder: '请输入关键词',
},
},
form: {
show: false,
component: {
props: {
clearable: true,
},
},
},
},
request_modular: {
title: '请求模块',
search: {
disabled: false,
},
type: 'input',
column:{
minWidth: 100,
},
form: {
disabled: true,
component: {
placeholder: '请输入请求模块',
},
},
},
request_path: {
title: '请求地址',
search: {
disabled: false,
},
type: 'input',
column:{
minWidth: 200,
},
form: {
disabled: true,
component: {
placeholder: '请输入请求地址',
},
},
},
request_body: {
column: {
showOverflowTooltip: true,
width: 200, //列宽
minWidth: 100, //最小列宽
},
title: '请求参数',
search: {
disabled: true,
},
disabled: true,
type: 'textarea',
form: {
component: {
props: {
type: 'textarea',
},
autosize: {
minRows: 2,
maxRows: 8,
},
placeholder: '请输入关键词',
},
},
},
request_method: {
title: '请求方法',
type: 'input',
search: {
disabled: false,
},
column:{
minWidth: 100,
},
form: {
disabled: true,
component: {
placeholder: '请输入请求方法',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
request_msg: {
title: '操作说明',
disabled: true,
form: {
component: {
span: 12,
},
},
},
request_ip: {
title: 'IP地址',
search: {
disabled: false,
},
type: 'input',
column:{
minWidth: 100,
},
form: {
disabled: true,
component: {
placeholder: '请输入IP地址',
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
request_browser: {
title: '请求浏览器',
type: 'input',
column:{
minWidth: 120,
},
form: {
disabled: true,
},
component: { props: { color: 'auto' } }, // 自动染色
},
response_code: {
title: '响应码',
search: {
disabled: true,
},
type: 'input',
column:{
minWidth: 100,
},
form: {
disabled: true,
},
component: { props: { color: 'auto' } }, // 自动染色
},
request_os: {
title: '操作系统',
disabled: true,
search: {
disabled: true,
},
type: 'input',
column:{
minWidth: 120,
},
form: {
disabled: true,
},
component: { props: { color: 'auto' } }, // 自动染色
},
json_result: {
title: '返回信息',
search: {
disabled: true,
},
type: 'input',
column:{
minWidth: 150,
},
form: {
disabled: true,
},
component: { props: { color: 'auto' } }, // 自动染色
},
creator_name: {
title: '操作人',
column:{
minWidth: 100,
},
form: {
disabled: true,
},
},
},
},
};
};

View File

@@ -0,0 +1,19 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
</fs-page>
</template>
<script lang="ts" setup name="operationLog">
import {ref, onMounted} from 'vue';
import {useFs} from '@fast-crud/fast-crud';
import {createCrudOptions} from './crud';
const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions});
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>

View File

@@ -0,0 +1,21 @@
import { request } from "/@/utils/service";
export function getCaptcha() {
return request({
url: '/api/captcha/',
method: 'get',
});
}
export function login(params: object) {
return request({
url: '/api/login/',
method: 'post',
data: params
});
}
export function getUserInfo() {
return request({
url: '/api/system/user/user_info/',
method: 'get',
});
}

View File

@@ -0,0 +1,234 @@
<template>
<el-form size="large" class="login-content-form">
<el-form-item class="login-animation1">
<el-input type="text" :placeholder="$t('message.account.accountPlaceholder1')" v-model="ruleForm.username" clearable autocomplete="off">
<template #prefix>
<el-icon class="el-input__icon"><ele-User /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation2">
<el-input
:type="isShowPassword ? 'text' : 'password'"
:placeholder="$t('message.account.accountPlaceholder2')"
v-model="ruleForm.password"
>
<template #prefix>
<el-icon class="el-input__icon"><ele-Unlock /></el-icon>
</template>
<template #suffix>
<i
class="iconfont el-input__icon login-content-password"
:class="isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
@click="isShowPassword = !isShowPassword"
>
</i>
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation3" v-if="isShowCaptcha">
<el-col :span="15">
<el-input
type="text"
maxlength="4"
:placeholder="$t('message.account.accountPlaceholder3')"
v-model="ruleForm.captcha"
clearable
autocomplete="off"
>
<template #prefix>
<el-icon class="el-input__icon"><ele-Position /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="1"></el-col>
<el-col :span="8">
<el-button class="login-content-captcha">
<el-image :src="ruleForm.captchaImgBase" @click="refreshCaptcha" />
</el-button>
</el-col>
</el-form-item>
<el-form-item class="login-animation4">
<el-button type="primary" class="login-content-submit" round @click.enter="loginClick" :loading="loading.signIn">
<span>{{ $t('message.account.accountBtnText') }}</span>
</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts">
import { toRefs, reactive, defineComponent, computed, onMounted, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import Cookies from 'js-cookie';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { initFrontEndControlRoutes } from '/@/router/frontEnd';
import { initBackEndControlRoutes } from '/@/router/backEnd';
import { Session } from '/@/utils/storage';
import { formatAxis } from '/@/utils/formatTime';
import { NextLoading } from '/@/utils/loading';
import * as loginApi from '/@/views/system/login/api';
import { useUserInfo } from '/@/stores/userInfo';
import { DictionaryStore } from '/@/stores/dictionary';
import { SystemConfigStore } from '/@/stores/systemConfig';
import { BtnPermissionStore } from '/@/plugin/permission/store.permission';
import { Md5 } from 'ts-md5';
export default defineComponent({
name: 'loginAccount',
setup() {
const { t } = useI18n();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { userInfos } = storeToRefs(useUserInfo());
const route = useRoute();
const router = useRouter();
const state = reactive({
isShowPassword: false,
ruleForm: {
username: '',
password: '',
captcha: '',
captchaKey: '',
captchaImgBase: '',
},
loading: {
signIn: false,
},
});
// 时间获取
const currentTime = computed(() => {
return formatAxis(new Date());
});
// 是否关闭验证码
const isShowCaptcha = computed(() => {
return SystemConfigStore().systemConfig['base.captcha_state'];
});
const getCaptcha = async () => {
loginApi.getCaptcha().then((ret: any) => {
state.ruleForm.captchaImgBase = ret.data.image_base;
state.ruleForm.captchaKey = ret.data.key;
});
};
const refreshCaptcha = async () => {
loginApi.getCaptcha().then((ret: any) => {
state.ruleForm.captchaImgBase = ret.data.image_base;
state.ruleForm.captchaKey = ret.data.key;
});
};
const loginClick = async () => {
loginApi.login({ ...state.ruleForm, password: Md5.hashStr(state.ruleForm.password) }).then((res: any) => {
if (res.code === 2000) {
Session.set('token', res.data.access);
Cookies.set('username', res.data.name);
if (!themeConfig.value.isRequestRoutes) {
// 前端控制路由2、请注意执行顺序
initFrontEndControlRoutes();
loginSuccess();
} else {
// 模拟后端控制路由isRequestRoutes 为 true则开启后端控制路由
// 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
initBackEndControlRoutes();
// 执行完 initBackEndControlRoutes再执行 signInSuccess
loginSuccess();
}
} else if (res.code === 4000) {
// 登录错误之后,刷新验证码
refreshCaptcha();
}
});
};
const getUserInfo = () => {
useUserInfo().setUserInfos();
};
const enterClickLogin = (e: any) => {
if (e.keyCode == 13 || e.keyCode == 100) {
loginClick();
}
};
// 登录成功后的跳转
const loginSuccess = () => {
//登录成功获取用户信息,获取系统字典数据
getUserInfo();
//获取所有字典
DictionaryStore().getSystemDictionarys();
// 初始化登录成功时间问候语
let currentTimeInfo = currentTime.value;
// 登录成功,跳到转首页
// 如果是复制粘贴的路径,非首页/登录页,那么登录成功后重定向到对应的路径中
if (route.query?.redirect) {
router.push({
path: <string>route.query?.redirect,
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
});
} else {
router.push('/');
}
// 登录成功提示
// 关闭 loading
state.loading.signIn = true;
const signInText = t('message.signInText');
ElMessage.success(`${currentTimeInfo}${signInText}`);
// 添加 loading防止第一次进入界面时出现短暂空白
NextLoading.start();
};
onMounted(() => {
getCaptcha();
//获取系统配置
SystemConfigStore().getSystemConfigs();
// window.addEventListener('keyup', enterClickLogin, false);
});
onUnmounted(() => {
// window.removeEventListener('keyup', enterClickLogin, false);
});
return {
refreshCaptcha,
loginClick,
loginSuccess,
isShowCaptcha,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.login-content-form {
margin-top: 20px;
@for $i from 1 through 4 {
.login-animation#{$i} {
opacity: 0;
animation-name: error-num;
animation-duration: 0.5s;
animation-fill-mode: forwards;
animation-delay: calc($i/10) + s;
}
}
.login-content-password {
display: inline-block;
width: 20px;
cursor: pointer;
&:hover {
color: #909399;
}
}
.login-content-captcha {
width: 100%;
padding: 0;
font-weight: bold;
letter-spacing: 5px;
}
.login-content-submit {
width: 100%;
letter-spacing: 2px;
font-weight: 300;
margin-top: 15px;
}
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<el-form size="large" class="login-content-form">
<el-form-item class="login-animation1">
<el-input type="text" :placeholder="$t('message.mobile.placeholder1')" v-model="ruleForm.username" clearable autocomplete="off">
<template #prefix>
<i class="iconfont icon-dianhua el-input__icon"></i>
</template>
</el-input>
</el-form-item>
<el-form-item class="login-animation2">
<el-col :span="15">
<el-input type="text" maxlength="4" :placeholder="$t('message.mobile.placeholder2')" v-model="ruleForm.code" clearable autocomplete="off">
<template #prefix>
<el-icon class="el-input__icon"><ele-Position /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="1"></el-col>
<el-col :span="8">
<el-button class="login-content-code">{{ $t('message.mobile.codeText') }}</el-button>
</el-col>
</el-form-item>
<el-form-item class="login-animation3">
<el-button round type="primary" class="login-content-submit">
<span>{{ $t('message.mobile.btnText') }}</span>
</el-button>
</el-form-item>
<div class="font12 mt30 login-animation4 login-msg">{{ $t('message.mobile.msgText') }}</div>
</el-form>
</template>
<script lang="ts">
import { toRefs, reactive, defineComponent } from 'vue';
// 定义接口来定义对象的类型
interface LoginMobileState {
username: any;
code: string | number | undefined;
}
// 定义对象与类型
const ruleForm: LoginMobileState = {
username: '',
code: '',
};
export default defineComponent({
name: 'loginMobile',
setup() {
const state = reactive({ ruleForm });
return {
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.login-content-form {
margin-top: 20px;
@for $i from 1 through 4 {
.login-animation#{$i} {
opacity: 0;
animation-name: error-num;
animation-duration: 0.5s;
animation-fill-mode: forwards;
animation-delay: calc($i/10) + s;
}
}
.login-content-code {
width: 100%;
padding: 0;
}
.login-content-submit {
width: 100%;
letter-spacing: 2px;
font-weight: 300;
margin-top: 15px;
}
.login-msg {
color: var(--el-text-color-placeholder);
}
}
</style>

View File

@@ -0,0 +1,59 @@
<template>
<div class="login-scan-container">
<div ref="qrcodeRef"></div>
<div class="font12 mt20 login-msg">{{ $t('message.scan.text') }}</div>
</div>
</template>
<script lang="ts">
import { ref, defineComponent, onMounted } from 'vue';
import QRCode from 'qrcodejs2-fixes';
export default defineComponent({
name: 'loginScan',
setup() {
const qrcodeRef = ref<HTMLElement | null>(null);
// 初始化生成二维码
const initQrcode = () => {
(qrcodeRef.value as HTMLElement).innerHTML = '';
new QRCode(qrcodeRef.value, {
text: `https://jq.qq.com/?_wv=1027&k=hUu2GeU1`,
width: 260,
height: 260,
colorDark: '#000000',
colorLight: '#ffffff',
});
};
// 页面加载时
onMounted(() => {
initQrcode();
});
return { qrcodeRef };
},
});
</script>
<style scoped lang="scss">
.login-scan-animation {
opacity: 0;
animation-name: error-num;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
.login-scan-container {
padding: 20px;
display: flex;
flex-direction: column;
text-align: center;
@extend .login-scan-animation;
animation-delay: 0.1s;
:deep(img) {
margin: auto;
}
.login-msg {
color: var(--el-text-color-placeholder);
@extend .login-scan-animation;
animation-delay: 0.2s;
}
}
</style>

View File

@@ -0,0 +1,255 @@
<template>
<div class="login-container flex">
<div class="login-left">
<div class="login-left-logo">
<img :src="logoMini" />
<div class="login-left-logo-text">
<span>{{ getThemeConfig.globalViceTitle }}</span>
<span class="login-left-logo-text-msg">{{ getThemeConfig.globalViceTitleMsg }}</span>
</div>
</div>
<div class="login-left-img">
<img :src="loginMain" />
</div>
<img :src="loginBg" class="login-left-waves" />
</div>
<div class="login-right flex">
<div class="login-right-warp flex-margin">
<span class="login-right-warp-one"></span>
<span class="login-right-warp-two"></span>
<div class="login-right-warp-mian">
<div class="login-right-warp-main-title">{{ getThemeConfig.globalTitle }} 欢迎您</div>
<div class="login-right-warp-main-form">
<div v-if="!state.isScan">
<el-tabs v-model="state.tabsActiveName">
<el-tab-pane :label="$t('message.label.one1')" name="account">
<Account />
</el-tab-pane>
<!-- TODO 手机号码登录未接入展示隐藏 -->
<!-- <el-tab-pane :label="$t('message.label.two2')" name="mobile">
<Mobile />
</el-tab-pane> -->
</el-tabs>
</div>
<Scan v-if="state.isScan" />
<div class="login-content-main-sacn" @click="state.isScan = !state.isScan">
<i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>
<div class="login-content-main-sacn-delta"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="loginIndex">
import { defineAsyncComponent, onMounted, reactive, computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { NextLoading } from '/@/utils/loading';
import logoMini from '/@/assets/logo-mini.svg';
import loginMain from '/@/assets/login-main.svg';
import loginBg from '/@/assets/login-bg.svg';
// 引入组件
const Account = defineAsyncComponent(() => import('/@/views/system/login/component/account.vue'));
const Mobile = defineAsyncComponent(() => import('/@/views/system/login/component/mobile.vue'));
const Scan = defineAsyncComponent(() => import('/@/views/system/login/component/scan.vue'));
// 定义变量内容
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const state = reactive({
tabsActiveName: 'account',
isScan: false,
});
// 获取布局配置信息
const getThemeConfig = computed(() => {
return themeConfig.value;
});
// 页面加载时
onMounted(() => {
NextLoading.done();
});
</script>
<style scoped lang="scss">
.login-container {
height: 100%;
background: var(--el-color-white);
.login-left {
flex: 1;
position: relative;
background-color: rgba(211, 239, 255, 1);
margin-right: 100px;
.login-left-logo {
display: flex;
align-items: center;
position: absolute;
top: 50px;
left: 80px;
z-index: 1;
animation: logoAnimation 0.3s ease;
img {
width: 52px;
height: 52px;
}
.login-left-logo-text {
display: flex;
flex-direction: column;
span {
margin-left: 10px;
font-size: 28px;
color: #26a59a;
}
.login-left-logo-text-msg {
font-size: 12px;
color: #32a99e;
}
}
}
.login-left-img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 52%;
img {
width: 100%;
height: 100%;
animation: error-num 0.6s ease;
}
}
.login-left-waves {
position: absolute;
top: 0;
right: -100px;
}
}
.login-right {
width: 700px;
.login-right-warp {
border: 1px solid var(--el-color-primary-light-3);
border-radius: 3px;
width: 500px;
height: 500px;
position: relative;
overflow: hidden;
background-color: var(--el-color-white);
.login-right-warp-one,
.login-right-warp-two {
position: absolute;
display: block;
width: inherit;
height: inherit;
&::before,
&::after {
content: '';
position: absolute;
z-index: 1;
}
}
.login-right-warp-one {
&::before {
filter: hue-rotate(0deg);
top: 0px;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(90deg, transparent, var(--el-color-primary));
animation: loginLeft 3s linear infinite;
}
&::after {
filter: hue-rotate(60deg);
top: -100%;
right: 2px;
width: 3px;
height: 100%;
background: linear-gradient(180deg, transparent, var(--el-color-primary));
animation: loginTop 3s linear infinite;
animation-delay: 0.7s;
}
}
.login-right-warp-two {
&::before {
filter: hue-rotate(120deg);
bottom: 2px;
right: -100%;
width: 100%;
height: 3px;
background: linear-gradient(270deg, transparent, var(--el-color-primary));
animation: loginRight 3s linear infinite;
animation-delay: 1.4s;
}
&::after {
filter: hue-rotate(300deg);
bottom: -100%;
left: 0px;
width: 3px;
height: 100%;
background: linear-gradient(360deg, transparent, var(--el-color-primary));
animation: loginBottom 3s linear infinite;
animation-delay: 2.1s;
}
}
.login-right-warp-mian {
display: flex;
flex-direction: column;
height: 100%;
.login-right-warp-main-title {
height: 130px;
line-height: 130px;
font-size: 27px;
text-align: center;
letter-spacing: 3px;
animation: logoAnimation 0.3s ease;
animation-delay: 0.3s;
color: var(--el-text-color-primary);
}
.login-right-warp-main-form {
flex: 1;
padding: 0 50px 50px;
.login-content-main-sacn {
position: absolute;
top: 0;
right: 0;
width: 50px;
height: 50px;
overflow: hidden;
cursor: pointer;
transition: all ease 0.3s;
color: var(--el-color-primary);
&-delta {
position: absolute;
width: 35px;
height: 70px;
z-index: 2;
top: 2px;
right: 21px;
background: var(--el-color-white);
transform: rotate(-45deg);
}
&:hover {
opacity: 1;
transition: all ease 0.3s;
color: var(--el-color-primary) !important;
}
i {
width: 47px;
height: 50px;
display: inline-block;
font-size: 48px;
position: absolute;
right: 1px;
top: 0px;
}
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,65 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/menu/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id + '/',
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(obj: DelReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'delete',
});
}
export function GetAllMenu(query: UserPageQuery) {
return request({
url: apiPrefix + 'get_all_menu/',
method: 'get',
params: query,
});
}
export function lazyLoadMenu(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function dragMenu(obj: AddReq) {
return request({
url: apiPrefix + 'drag_menu/',
method: 'post',
data: obj,
});
}

View File

@@ -0,0 +1,41 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/menu_button/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: any) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,214 @@
import {
CrudOptions,
AddReq,
DelReq,
EditReq,
dict,
CrudExpose,
CreateCrudOptionsRet,
CreateCrudOptionsProps,
UserPageQuery,
} from '@fast-crud/fast-crud';
import _ from 'lodash-es';
import * as api from './api';
import { request } from '/@/utils/service';
//此处为crudOptions配置
export const createCrudOptions = function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
if(context!.selectOptions.value.id){
return await api.GetList({ menu: context!.selectOptions.value.id } as any);
}else{
return undefined
}
};
const editRequest = async ({ form, row }: EditReq) => {
return await api.UpdateObj({ ...form, menu: row.menu });
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj({ ...form, ...{ menu: context!.selectOptions.value.id } });
};
return {
crudOptions: {
search: {
container: {
action: {
//按钮栏配置
col: {
span: 8
}
}
},
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
},
remove: {
iconRight: 'Delete',
type: 'text',
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
form: {
col: { span: 24 },
labelWidth: '100px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
search: {
title: '关键词',
column: { show: false },
type: 'text',
search: { show: true },
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
id: {
title: 'ID',
type: 'text',
column: { show: false },
search: { show: false },
form: { show: false },
},
name: {
title: '权限名称',
type: 'text',
search: { show: true },
column: {
minWidth: 120,
sortable: true,
},
form: {
rules: [{ required: true, message: '权限名称必填' }],
component: {
placeholder: '输入权限名称搜索',
props: {
clearable: true,
allowCreate: true,
filterable: true,
},
},
helper: {
render(h) {
return <el-alert title="手动输入" type="warning" description="页面中按钮的名称或者自定义一个名称" />;
},
},
},
},
value: {
title: '权限值',
type: 'text',
search: { show: false },
column: {
width: 120,
sortable: true,
},
form: {
rules: [{ required: true, message: '权限标识必填' }],
placeholder: '输入权限标识',
helper: {
render(h) {
return <el-alert title="唯一值" type="warning" description="用于判断前端按钮权限或接口权限" />;
},
},
},
},
method: {
title: '请求方式',
search: { show: false },
type: 'dict-select',
column: {
width: 120,
sortable: true,
},
dict: dict({
data: [
{ label: 'GET', value: 0 },
{ label: 'POST', value: 1, color: 'success' },
{ label: 'PUT', value: 2, color: 'warning' },
{ label: 'DELETE', value: 3, color: 'danger' },
],
}),
form: {
rules: [{ required: true, message: '必填项' }],
},
},
api: {
title: '接口地址',
search: { show: false },
type: 'dict-select',
dict: dict({
getData() {
return request({ url: '/swagger.json' }).then((res: any) => {
const ret = Object.keys(res.paths);
const data = [];
for (const item of ret) {
const obj: any = {};
obj.label = item;
obj.value = item;
data.push(obj);
}
return data;
});
},
}),
column: {
minWidth: 200,
sortable: true,
},
form: {
rules: [{ required: true, message: '必填项' }],
component: {
props: {
allowCreate: true,
filterable: true,
clearable: true,
},
},
helper: {
render(h) {
return <el-alert title="请正确填写,以免请求时被拦截。匹配单例使用正则,例如:/api/xx/.*?/" type="warning" />;
},
},
},
},
},
},
};
};

View File

@@ -0,0 +1,29 @@
<template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</template>
<script lang="ts" setup>
import { ref, defineProps, watch } from 'vue';
import { useFs } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
// 当前选择的菜单信息
let selectOptions: any = ref({ name: null });
const props = defineProps<{
selectMenu: object;
}>();
const { crudRef, crudBinding, crudExpose, context } = useFs({ createCrudOptions, context: { selectOptions } });
const { doRefresh,setTableData } = crudExpose;
watch(props.selectMenu, (val: any) => {
if (!val.is_catalog && val.id) {
selectOptions.value = val;
doRefresh();
}else{
//清空表格数据
setTableData([])
}
});
defineExpose({ selectOptions });
</script>

View File

@@ -0,0 +1,31 @@
import * as api from './api';
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from '@fast-crud/fast-crud';
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
columns: {},
},
};
}

View File

@@ -0,0 +1,400 @@
<template>
<fs-page>
<splitpanes>
<pane max-size="20" min-size="16">
<el-card :body-style="{ height: '100%' }">
<p class="font-mono font-black text-center text-xl pb-5">
菜单列表
<el-tooltip effect="dark" :content="content" placement="right">
<el-icon>
<QuestionFilled/>
</el-icon>
</el-tooltip>
</p>
<el-input v-model="filterText" :placeholder="placeholder"/>
<el-tree ref="treeRef" class="font-mono font-bold leading-6 text-7xl" :data="data"
:props="treeProps"
:filter-node-method="filterNode" :load="loadNode" :allow-drag="allowDrag"
:allow-drop="allowDrop"
@node-drop="nodeDrop" lazy icon="ArrowRightBold" :indent="12" draggable
@node-click="handleNodeClick">
<template #default="{ node, data }">
<span v-if="data.status" class="text-center font-black font-normal">
<SvgIcon :name="node.data.icon"/>&nbsp;{{ node.label }}
</span>
<span v-else class="text-center font-black text-red-700 font-normal">
<SvgIcon :name="node.data.icon"/>&nbsp;{{ node.label }}
</span>
</template>
</el-tree>
</el-card>
</pane>
<pane min-size="30">
<el-card :body-style="{ height: '100%' }">
<el-form ref="formRef" :rules="rules" :model="form" label-width="80px"
label-position="right">
<el-alert :title="content" type="success" effect="dark" :closable="false" center/>
<el-divider>
<strong>菜单配置</strong>
</el-divider>
<el-form-item label="菜单ID" prop="id">
<el-input v-model="form.id" disabled/>
</el-form-item>
<el-form-item label="父级ID" prop="parent">
<el-input v-model="form.parent"/>
</el-form-item>
<el-form-item required label="菜单名称" prop="name">
<el-input v-model="form.name"/>
</el-form-item>
<el-form-item label="组件地址" prop="component">
<el-autocomplete class="w-full" v-model="form.component"
:fetch-suggestions="querySearch" :trigger-on-focus="false"
clearable
debounce="100"
placeholder="输入组件地址"/>
</el-form-item>
<el-form-item required label="Url" prop="web_path">
<el-input v-model="form.web_path"/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" controls-position="right"/>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="侧边可见">
<el-radio-group v-model="form.visible">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="缓存">
<el-radio-group v-model="form.cache">
<el-radio :label="true">启用</el-radio>
<el-radio :label="false">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="图标" prop="icon">
<IconSelector clearable v-model="form.icon"/>
</el-form-item>
</el-form>
<el-divider></el-divider>
<div>
<el-button @click="saveMenu()" type="primary" round>保存</el-button>
<el-button @click="newMenu()" type="success" round :disabled="!form.id">新建</el-button>
<el-button @click="addChildMenu()" type="warning" round :disabled="!form.id">添加子级
</el-button>
<!-- <el-button @click="addSameLevelMenu()" type="warning" round>添加同级</el-button>-->
<el-button @click="deleteMenu()" type="danger" round :disabled="!form.id">删除菜单
</el-button>
</div>
</el-card>
</pane>
<pane min-size="30">
<el-card :body-style="{ height: '100%' }">
<menuButton :select-menu="form"/>
</el-card>
</pane>
</splitpanes>
</fs-page>
</template>
<script lang="ts" setup name="menu">
import {Splitpanes, Pane} from 'splitpanes';
import 'splitpanes/dist/splitpanes.css';
import * as api from './api';
import * as menuButoonApi from './components/menuButton/api';
import {ElForm, ElTree, FormRules, ElMessageBox} from 'element-plus';
import {ref, onMounted, watch, reactive, toRaw, defineAsyncComponent, nextTick, shallowRef, onActivated} from 'vue';
import XEUtils from 'xe-utils';
import {errorMessage, successMessage} from '../../../utils/message';
interface Tree {
id: number;
name: string;
status: boolean;
children?: Tree[];
}
interface APIResponseData {
code?: number;
data: [];
msg?: string;
}
interface Form<T> {
[key: string]: T;
}
interface ComponentFileItem {
value: string;
label: string;
}
// 引入组件
const menuButton = defineAsyncComponent(() => import('./components/menuButton/index.vue'));
const IconSelector = defineAsyncComponent(() => import('/@/components/iconSelector/index.vue'));
const SvgIcon = defineAsyncComponent(() => import('/@/components/svgIcon/index.vue'));
const placeholder = ref('请输入菜单名称');
const filterText = ref('');
const treeRef = ref<InstanceType<typeof ElTree>>();
const treeProps = {
children: 'children',
label: 'name',
icon: 'icon',
isLeaf: (data: Tree[], node: Node) => {
// @ts-ignore
if (node.data.is_catalog) {
return false;
} else {
return true;
}
},
};
const validateWebPath = (rule: string, value: string, callback: Function) => {
let pattern = /^\/.*?/;
if (!pattern.test(value)) {
callback(new Error('请输入正确的地址'));
} else {
callback();
}
};
watch(filterText, (val) => {
treeRef.value!.filter(val);
});
const filterNode = (value: string, data: Tree) => {
if (!value) return true;
return toRaw(data).name.indexOf(value) !== -1;
};
// 懒加载
const loadNode = (node: Node, resolve: (data: Tree[]) => void) => {
// @ts-ignore
if (node.level !== 0) {
// @ts-ignore
api.lazyLoadMenu({parent: node.data.id}).then((res: APIResponseData) => {
resolve(res.data);
});
}
};
// 判断是否可以拖动
const allowDrag = (node: Node) => {
// @ts-ignore
if (node.data.is_catalog) {
return false;
} else {
return true;
}
};
// 判断是否可以被放置
const allowDrop = (draggingNode: Node, dropNode: Node, type: string) => {
// @ts-ignore
if (!dropNode.isLeaf) {
return true;
}
};
const nodeDrop = (draggingNode: Node, dropNode: Node, dropType: string, event: any) => {
// @ts-ignore
if (!dropNode.isLeaf) {
// @ts-ignore
api.dragMenu({menu_id: draggingNode.data.id, parent_id: dropNode.data.id}).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
}
};
let data = ref([]);
let isAddNewMenu = ref(false); // 判断当前是新增菜单,还是更新保存当前菜单
const permissionDrawerVisible = ref(false);
const content = `
1.红色菜单代表状态禁用;
2.添加菜单,如果是目录,组件地址为空即可;
3.添加根节点菜单父级ID为空即可;
4.支持拖拽菜单;
`;
let form: Form<any> = reactive({
id: '',
parent: '',
name: '',
component: '',
web_path: '',
sort: '',
status: true,
is_catalog: false,
permission: '',
icon: '',
visible: true,
cache: true,
});
let menuPermissonList = ref([]);
const formRef = ref<InstanceType<typeof ElForm>>();
const querySearch = (queryString: string, cb: any) => {
const files: any = import.meta.glob('@views/**/*.vue');
let fileLists: Array<any> = [];
Object.keys(files).forEach((queryString: string) => {
fileLists.push({
label: queryString.replace(/(\.\/|\.vue)/g, ''),
value: queryString.replace(/(\.\/|\.vue)/g, ''),
});
});
const results = queryString ? fileLists.filter(createFilter(queryString)) : fileLists;
// 统一去掉/src/views/前缀
results.forEach((val) => {
val.label = val.label.replace('/src/views/', '');
val.value = val.value.replace('/src/views/', '');
});
cb(results);
};
const createFilter = (queryString: string) => {
return (file: ComponentFileItem) => {
return file.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1;
};
};
const rules = reactive<FormRules>({
// @ts-ignore
web_path: [{validator: validateWebPath, trigger: 'blur'}],
});
const getData = () => {
api.GetList({}).then((ret: APIResponseData) => {
const responseData = ret.data;
const result = XEUtils.toArrayTree(responseData, {
parentKey: 'parent',
children: 'children',
strict: true,
});
data.value = result;
});
};
const getPermissions = (menu: object) => {
menuButoonApi.GetList(menu).then((res: APIResponseData) => {
menuPermissonList.value = res.data;
});
};
const saveMenu = () => {
formRef.value?.validate((valid, fields) => {
if (valid) {
if (!isAddNewMenu.value) {
// 保存菜单
form.component == '' ? (form.is_catalog = true) : (form.is_catalog = false);
api.UpdateObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
} else {
// 新增菜单
form.component == '' ? (form.is_catalog = true) : (form.is_catalog = false);
api.AddObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
}
} else {
errorMessage('请填写检查表单');
}
});
};
const newMenu = () => {
formRef.value?.resetFields();
isAddNewMenu.value = true;
};
const addChildMenu = () => {
let parentId = form.id;
formRef.value?.resetFields();
form.parent = parentId;
isAddNewMenu.value = true;
};
const addSameLevelMenu = () => {
let parentId = form.parent;
formRef.value?.resetFields();
form.parent = parentId;
isAddNewMenu.value = true;
};
const deleteMenu = () => {
ElMessageBox.confirm(
'您确认删除该菜单项吗?',
'温馨提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
api.DelObj(form).then((res: APIResponseData) => {
successMessage(res.msg as string);
getData();
});
})
};
const handleNodeClick = (data: any, node: any, prop: any) => {
Object.keys(toRaw(data)).forEach((key: string) => {
form[key] = data[key];
});
delete form.component_name;
form.id = data.id;
isAddNewMenu.value = false;
// 点击tree node时加载对应的权限菜单
// getPermissions({ menu: form.id });
};
const addPermission = () => {
!form.is_catalog ? (permissionDrawerVisible.value = true) : errorMessage('目录没有菜单权限');
};
const drawerClose = () => {
permissionDrawerVisible.value = false;
};
// 页面打开后获取列表数据
onMounted(() => {
getData();
});
onActivated(() => {
console.log('keep-alive成功')
});
</script>
<style lang="scss" scoped>
.el-row {
height: 100%;
.el-col {
height: 100%;
}
}
.el-card {
height: 100%;
}
.font-normal {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
}
</style>

View File

@@ -0,0 +1,60 @@
import {request} from '/@/utils/service';
import {PageQuery, AddReq, DelReq, EditReq, InfoReq} from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/message_center/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id + '/',
method: 'get',
});
}
/**
* 获取自己接收的消息
* @param query
* @returns {*}
* @constructor
*/
export function GetSelfReceive (query:PageQuery) {
return request({
url: apiPrefix + 'get_self_receive/',
method: 'get',
params: query
})
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: {id},
});
}

View File

@@ -0,0 +1,363 @@
import * as api from './api';
import { dict, useCompute, PageQuery, AddReq, DelReq, EditReq, CrudExpose, CrudOptions } from '@fast-crud/fast-crud';
import tableSelector from '/@/components/tableSelector/index.vue';
import {shallowRef, computed, ref, inject} from 'vue';
import manyToMany from '/@/components/manyToMany/index.vue';
const { compute } = useCompute();
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
export const createCrudOptions = function ({ crudExpose, tabActivted }: { crudExpose: CrudExpose; tabActivted: any }): CreateCrudOptionsTypes {
const pageRequest = async (query: PageQuery) => {
if (tabActivted.value === 'receive') {
return await api.GetSelfReceive(query);
}
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
const viewRequest = async ({ row }: { row: any }) => {
return await api.GetObj(row.id);
};
const IsReadFunc = computed(() => {
return tabActivted.value === 'receive';
});
//权限判定
const hasPermissions = inject("$hasPermissions")
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
fixed:'right',
width:150,
buttons: {
edit: {
show: false,
},
view: {
text:"查看",
type:'text',
iconRight:'View',
show:hasPermissions("messageCenter:Search"),
click({ index, row }) {
crudExpose.openView({ index, row });
if (tabActivted.value === 'receive') {
viewRequest({ row });
crudExpose.doRefresh();
}
},
},
remove: {
iconRight: 'Delete',
type: 'text',
show:hasPermissions('messageCenter:Delete')
},
},
},
columns: {
id: {
title: 'id',
form: {
show: false,
},
},
title: {
title: '标题',
search: {
show: true,
},
type: ['text', 'colspan'],
column:{
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: { span: 24, placeholder: '请输入标题' },
},
},
is_read: {
title: '是否已读',
type: 'dict-select',
column: {
show: IsReadFunc.value,
},
dict: dict({
data: [
{ label: '已读', value: true, color: 'success' },
{ label: '未读', value: false, color: 'danger' },
],
}),
form: {
show: false,
},
},
target_type: {
title: '目标类型',
type: ['dict-radio', 'colspan'],
column:{
minWidth: 120,
},
dict: dict({
data: [
{ value: 0, label: '按用户' },
{ value: 1, label: '按角色' },
{
value: 2,
label: '按部门',
},
{ value: 3, label: '通知公告' },
],
}),
form: {
component: {
optionName: 'el-radio-button',
},
rules: [
{
required: true,
message: '必选项',
// @ts-ignore
trigger: ['blur', 'change'],
},
],
},
},
target_user: {
title: '目标用户',
search: {
disabled: true,
},
form: {
component: {
name: shallowRef(tableSelector),
vModel: 'modelValue',
displayLabel: compute(({ row }) => {
if (row) {
return row.user_info;
}
return null;
}),
tableConfig: {
url: '/api/system/user/',
label: 'name',
value: 'id',
isMultiple: true,
columns: [
{
prop: 'name',
label: '用户名称',
width: 120,
},
{
prop: 'phone',
label: '用户电话',
width: 120,
},
],
},
},
show: compute(({ form }) => {
return form.target_type === 0;
}),
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
},
column: {
show: false,
component: {
name: shallowRef(manyToMany),
vModel: 'modelValue',
bindValue: compute(({ row }) => {
return row.user_info;
}),
displayLabel: 'name',
},
},
},
target_role: {
title: '目标角色',
search: {
disabled: true,
},
width: 130,
form: {
component: {
name: shallowRef(tableSelector),
vModel: 'modelValue',
displayLabel: compute(({ row }) => {
if (row) {
return row.role_info;
}
return null;
}),
tableConfig: {
url: '/api/system/role/',
label: 'name',
value: 'id',
isMultiple: true,
columns: [
{
prop: 'name',
label: '角色名称',
},
{
prop: 'key',
label: '权限标识',
},
],
},
},
show: compute(({ form }) => {
return form.target_type === 1;
}),
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
},
column: {
show: false,
component: {
name: shallowRef(manyToMany),
vModel: 'modelValue',
bindValue: compute(({ row }) => {
return row.role_info;
}),
displayLabel: 'name',
},
},
},
target_dept: {
title: '目标部门',
search: {
disabled: true,
},
width: 130,
type: 'table-selector',
form: {
component: {
name: shallowRef(tableSelector),
vModel: 'modelValue',
displayLabel: compute(({ form }) => {
return form.target_dept_name;
}),
tableConfig: {
url: '/api/system/dept/all_dept/',
label: 'name',
value: 'id',
isTree: true,
isMultiple: true,
columns: [
{
prop: 'name',
label: '部门名称',
},
{
prop: 'status_label',
label: '状态',
},
{
prop: 'parent_name',
label: '父级部门',
},
],
},
},
show: compute(({ form }) => {
return form.target_type === 2;
}),
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
},
column: {
show: false,
component: {
name: shallowRef(manyToMany),
vModel: 'modelValue',
bindValue: compute(({ row }) => {
return row.dept_info;
}),
displayLabel: 'name',
},
},
},
content: {
title: '内容',
column: {
width: 300,
show: false,
},
type: ['editor-wang5', 'colspan'],
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
disabled: true,
id: '1', // 当同一个页面有多个editor时需要配置不同的id
editorConfig: {
// 是否只读
readOnly: compute((context) => {
const { mode } = context;
if (mode === 'add') {
return false;
}
return true;
}),
},
uploader: {
type: 'form',
buildUrl(res: any) {
return res.url;
},
},
},
},
},
},
},
};
};

View File

@@ -0,0 +1,43 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #header-middle>
<el-tabs v-model="tabActivted" @tab-click="onTabClick">
<el-tab-pane label="我的发布" name="send"></el-tab-pane>
<el-tab-pane label="我的接收" name="receive"></el-tab-pane>
</el-tabs>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup name="messageCenter">
import {ref, onMounted} from 'vue';
import {useExpose, useCrud} from '@fast-crud/fast-crud';
import {createCrudOptions} from './crud';
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const {crudExpose} = useExpose({crudRef, crudBinding});
//tab选择
const tabActivted = ref('send')
const onTabClick= (tab:any)=> {
const { paneName } = tab
tabActivted.value = paneName
crudExpose.doRefresh();
}
// 你的crud配置
const {crudOptions} = createCrudOptions({crudExpose,tabActivted});
// 初始化crud配置
const {resetCrudOptions} = useCrud({crudExpose, crudOptions});
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>

View File

@@ -0,0 +1,63 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
import { apiPrefix } from '/@/views/system/messageCenter/api';
export function GetUserInfo(query: PageQuery) {
return request({
url: '/api/system/user/user_info/',
method: 'get',
params: query,
});
}
/**
* 更新用户信息
* @param data
*/
export function updateUserInfo(data: AddReq) {
return request({
url: '/api/system/user/update_user_info/',
method: 'put',
data: data,
});
}
/**
* 获取自己接收的消息
* @param query
* @returns {*}
* @constructor
*/
export function GetSelfReceive(query: PageQuery) {
return request({
url: '/api/system/message_center/get_self_receive/',
method: 'get',
params: query,
});
}
/***
* 修改密码
* @param data
*/
export function UpdatePassword(data: EditReq) {
return request({
url: '/api/system/user/change_password/',
method: 'put',
data: data,
});
}
/***
* 上传头像
* @param data
*/
export function uploadAvatar(data: AddReq) {
return request({
url: 'api/system/file/',
method: 'post',
data: data,
headers: {
'Content-Type': 'multipart/form-data',
},
});
}

View File

@@ -0,0 +1,526 @@
<template>
<div class="personal layout-pd">
<el-row>
<!-- 个人信息 -->
<el-col :xs="24" :sm="16">
<el-card shadow="hover" header="个人信息">
<div class="personal-user">
<div class="personal-user-left">
<avatarSelector v-model="selectImgVisible" @uploadImg="uploadImg" ref="avatarSelectorRef"></avatarSelector>
</div>
<div class="personal-user-right">
<el-row>
<el-col :span="24" class="personal-title mb18"
>{{ currentTime }}{{ state.personalForm.username }}生活变的再糟糕也不妨碍我变得更好
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="8" class="personal-item mb6">
<div class="personal-item-label">昵称</div>
<div class="personal-item-value">{{ state.personalForm.name }}</div>
</el-col>
<el-col :xs="24" :sm="16" class="personal-item mb6">
<div class="personal-item-label">部门</div>
<div class="personal-item-value">
<el-tag>{{ state.personalForm.dept_info.dept_name }}</el-tag>
</div>
</el-col>
</el-row>
</el-col>
<el-col :span="24">
<el-row>
<el-col :xs="24" :sm="24" class="personal-item mb6">
<div class="personal-item-label">角色</div>
<div class="personal-item-value">
<el-tag v-for="(item, index) in state.personalForm.role_info" :key="index">{{ item.name }}</el-tag>
</div>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</div>
</el-card>
</el-col>
<!-- 消息通知 -->
<el-col :xs="24" :sm="8" class="pl15 personal-info">
<el-card shadow="hover">
<template #header>
<span>消息通知</span>
<span class="personal-info-more" @click="msgMore">更多</span>
</template>
<div class="personal-info-box">
<ul class="personal-info-ul">
<li v-for="(v, k) in state.newsInfoList" :key="k" class="personal-info-li">
<div class="personal-info-li-title">[{{ v.creator_name }},{{ v.create_datetime }}] {{ v.title }}</div>
</li>
</ul>
</div>
</el-card>
</el-col>
<!-- 更新信息 -->
<el-col :span="24">
<el-card shadow="hover" class="mt15 personal-edit" header="更新信息">
<div class="personal-edit-title">基本信息</div>
<el-form :model="state.personalForm" ref="userInfoFormRef" :rules="rules" size="default" label-width="50px" class="mt35 mb35">
<el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="昵称" prop="name">
<el-input v-model="state.personalForm.name" placeholder="请输入昵称" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="邮箱">
<el-input v-model="state.personalForm.email" placeholder="请输入邮箱" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="手机" prop="mobile">
<el-input v-model="state.personalForm.mobile" placeholder="请输入手机" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" class="mb20">
<el-form-item label="性别">
<el-select v-model="state.personalForm.gender" placeholder="请选择性别" clearable class="w100">
<!-- <el-option label="男" :value="1"></el-option>-->
<!-- <el-option label="女" :value="0"></el-option>-->
<!-- <el-option label="保密" :value="2"></el-option>-->
<el-option v-for="(item,index) in genderList" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-form-item>
<el-button type="primary" @click="submitForm">
<el-icon>
<ele-Position />
</el-icon>
更新个人信息
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="personal-edit-title mb15">账号安全</div>
<div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">账户密码</div>
<div class="personal-edit-safe-item-left-value">当前密码强度</div>
</div>
<div class="personal-edit-safe-item-right">
<el-button text type="primary" @click="passwordFormShow = true">立即修改</el-button>
</div>
</div>
</div>
<div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">密保手机</div>
<div class="personal-edit-safe-item-left-value">已绑定手机{{ state.personalForm.mobile }}</div>
</div>
<div class="personal-edit-safe-item-right">
<!-- <el-button text type="primary">立即修改</el-button>-->
</div>
</div>
</div>
<div class="personal-edit-safe-box">
<div class="personal-edit-safe-item">
<div class="personal-edit-safe-item-left">
<div class="personal-edit-safe-item-left-label">绑定邮箱</div>
<div class="personal-edit-safe-item-left-value">已绑定邮箱{{ state.personalForm.email }}</div>
</div>
<div class="personal-edit-safe-item-right">
<!-- <el-button text type="primary">立即设置</el-button>-->
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 密码修改-->
<el-dialog v-model="passwordFormShow" title="密码修改">
<el-form
ref="userPasswordFormRef"
:model="userPasswordInfo"
required-asterisk
label-width="100px"
label-position="left"
:rules="passwordRules"
center
>
<el-form-item label="原密码" required prop="oldPassword">
<el-input v-model="userPasswordInfo.oldPassword" placeholder="请输入原始密码" clearable></el-input>
</el-form-item>
<el-form-item required prop="newPassword" label="新密码">
<el-input type="password" v-model="userPasswordInfo.newPassword" placeholder="请输入新密码" show-password clearable></el-input>
</el-form-item>
<el-form-item required prop="newPassword2" label="确认密码">
<el-input type="password" v-model="userPasswordInfo.newPassword2" placeholder="请再次输入新密码" show-password clearable></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="settingPassword"> <i class="fa fa-check"></i>提交 </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="personal">
import { reactive, computed, onMounted, ref, defineAsyncComponent } from 'vue';
import { formatAxis } from '/@/utils/formatTime';
import * as api from './api';
import { ElMessage } from 'element-plus';
import { getBaseURL } from '/@/utils/baseUrl';
import { Session } from '/@/utils/storage';
import { useRouter } from 'vue-router';
import { useUserInfo } from '/@/stores/userInfo';
import { successMessage } from '/@/utils/message';
import {dictionary} from "/@/utils/dictionary";
// 头像裁剪组件
const avatarSelector = defineAsyncComponent(() => import('/@/components/avatarSelector/index.vue'));
const avatarSelectorRef = ref(null);
// 当前时间提示语
const currentTime = computed(() => {
return formatAxis(new Date());
});
const userInfoFormRef = ref();
const rules = reactive({
name: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
mobile: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确手机号' }],
});
let selectImgVisible = ref(false);
const state = reactive<PersonalState>({
newsInfoList: [],
personalForm: {
avatar: '',
username: '',
name: '',
email: '',
mobile: '',
gender: '',
dept_info: {
dept_id: 0,
dept_name: '',
},
role_info: [
{
id: 0,
name: '',
},
],
},
});
/**
* 跳转消息中心
*/
const route = useRouter();
const msgMore = () => {
route.push({ path: '/messageCenter' });
};
const genderList = ref();
/**
* 获取用户个人信息
*/
const getUserInfo = function () {
api.GetUserInfo({}).then((res: any) => {
const { data } = res;
genderList.value = dictionary('gender')
state.personalForm.avatar = data.avatar || '';
state.personalForm.username = data.username || '';
state.personalForm.name = data.name || '';
state.personalForm.email = data.email || '';
state.personalForm.mobile = data.mobile || '';
state.personalForm.gender = data.gender;
state.personalForm.dept_info.dept_name = data.dept_info.dept_name || '';
state.personalForm.role_info = data.role_info || [];
});
};
/**
* 更新用户信息
* @param formEl
*/
const submitForm = async () => {
if (!userInfoFormRef.value) return;
await userInfoFormRef.value.validate((valid, fields) => {
if (valid) {
api.updateUserInfo(state.personalForm).then((res: any) => {
ElMessage.success('更新成功');
getUserInfo();
});
} else {
ElMessage.error('表单验证失败,请检查~');
}
});
};
/**
* 获取消息通知
*/
const getMsg = () => {
api.GetSelfReceive({}).then((res: any) => {
const { data } = res;
state.newsInfoList = data || [];
});
};
onMounted(() => {
getUserInfo();
getMsg();
});
/**************************密码修改部分************************/
const passwordFormShow = ref(false);
const userPasswordFormRef = ref();
const userPasswordInfo = reactive({
oldPassword: '',
newPassword: '',
newPassword2: '',
});
const validatePass = (rule, value, callback) => {
const pwdRegex = new RegExp('(?=.*[0-9])(?=.*[a-zA-Z]).{8,30}');
if (value === '') {
callback(new Error('请输入密码'));
} else if (value === userPasswordInfo.oldPassword) {
callback(new Error('原密码与新密码一致'));
} else if (!pwdRegex.test(value)) {
callback(new Error('您的密码复杂度太低(密码中必须包含字母、数字)'));
} else {
if (userPasswordInfo.newPassword2 !== '') {
userPasswordFormRef.value.validateField('newPassword2');
}
callback();
}
};
const validatePass2 = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'));
} else if (value !== userPasswordInfo.newPassword) {
callback(new Error('两次输入密码不一致!'));
} else {
callback();
}
};
const passwordRules = reactive({
oldPassword: [
{
required: true,
message: '请输入原密码',
trigger: 'blur',
},
],
newPassword: [{ validator: validatePass, trigger: 'blur' }],
newPassword2: [{ validator: validatePass2, trigger: 'blur' }],
});
/**
* 重新设置密码
*/
const settingPassword = () => {
userPasswordFormRef.value.validate((valid) => {
if (valid) {
api.UpdatePassword(userPasswordInfo).then((res: any) => {
ElMessage.success('密码修改成功');
});
} else {
// 校验失败
// 登录表单校验失败
ElMessage.error('表单校验失败,请检查');
}
});
};
const uploadImg = (data: any) => {
let formdata = new FormData();
formdata.append('file', data);
api.uploadAvatar(formdata).then((res: any) => {
if (res.code === 2000) {
selectImgVisible.value = false;
state.personalForm.avatar = getBaseURL() + res.data.url;
api.updateUserInfo(state.personalForm).then((res: any) => {
successMessage('更新成功');
getUserInfo();
useUserInfo().updateUserInfos();
// @ts-ignore
avatarSelectorRef.value.updateAvatar(state.personalForm.avatar);
});
}
});
};
</script>
<style scoped lang="scss">
@import '/@/theme/mixins/index.scss';
.personal {
.personal-user {
height: 130px;
display: flex;
align-items: center;
.personal-user-left {
width: 100px;
height: 130px;
border-radius: 3px;
:deep(.el-upload) {
height: 100%;
}
.personal-user-left-upload {
img {
width: 100%;
height: 100%;
border-radius: 3px;
}
&:hover {
img {
animation: logoAnimation 0.3s ease-in-out;
}
}
}
}
.personal-user-right {
flex: 1;
padding: 0 15px;
.personal-title {
font-size: 18px;
@include text-ellipsis(1);
}
.personal-item {
display: flex;
align-items: center;
font-size: 13px;
.personal-item-label {
color: var(--el-text-color-secondary);
@include text-ellipsis(1);
}
.personal-item-value {
@include text-ellipsis(1);
}
}
}
}
.personal-info {
.personal-info-more {
float: right;
color: var(--el-text-color-secondary);
font-size: 13px;
&:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
.personal-info-box {
height: 130px;
overflow: hidden;
.personal-info-ul {
list-style: none;
.personal-info-li {
font-size: 13px;
padding-bottom: 10px;
.personal-info-li-title {
display: inline-block;
@include text-ellipsis(1);
color: var(--el-text-color-secondary);
text-decoration: none;
}
& a:hover {
color: var(--el-color-primary);
cursor: pointer;
}
}
}
}
}
.personal-recommend-row {
.personal-recommend-col {
.personal-recommend {
position: relative;
height: 100px;
border-radius: 3px;
overflow: hidden;
cursor: pointer;
&:hover {
i {
right: 0px !important;
bottom: 0px !important;
transition: all ease 0.3s;
}
}
i {
position: absolute;
right: -10px;
bottom: -10px;
font-size: 70px;
transform: rotate(-30deg);
transition: all ease 0.3s;
}
.personal-recommend-auto {
padding: 15px;
position: absolute;
left: 0;
top: 5%;
color: var(--next-color-white);
.personal-recommend-msg {
font-size: 12px;
margin-top: 10px;
}
}
}
}
}
.personal-edit {
.personal-edit-title {
position: relative;
padding-left: 10px;
color: var(--el-text-color-regular);
&::after {
content: '';
width: 2px;
height: 10px;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
background: var(--el-color-primary);
}
}
.personal-edit-safe-box {
border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
padding: 15px 0;
.personal-edit-safe-item {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.personal-edit-safe-item-left {
flex: 1;
overflow: hidden;
.personal-edit-safe-item-left-label {
color: var(--el-text-color-regular);
margin-bottom: 5px;
}
.personal-edit-safe-item-left-value {
color: var(--el-text-color-secondary);
@include text-ellipsis(1);
margin-right: 15px;
}
}
}
&:last-of-type {
padding-bottom: 0;
border-bottom: none;
}
}
}
}
</style>

View File

@@ -0,0 +1,41 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/role/';
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,117 @@
import { request } from "/@/utils/service";
/**
* 获取角色所拥有的菜单
* @param params
*/
export function GetMenu(params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_get_menu/',
method: 'get',
params:params
});
}
/***
* 新增权限
* @param data
* @constructor
*/
export function SaveMenuPermission(data:any) {
return request({
url: '/api/system/role_menu_permission/save_auth/',
method: 'post',
data:data
});
}
/**
* 获取菜单下的按钮
* @param params
* @constructor
*/
export function GetMenuButton(params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_menu_get_button/',
method: 'get',
params:params
});
}
/***
* 根据角色获取已授权的菜单
* @param params
* @constructor
*/
export function role_to_menu (params:any={}) {
return request({
url: '/api/system/role_menu_button_permission/role_to_menu/',
method: 'get',
params: params
})
}
/***
* 根据角色获取数据权限范围
* @constructor
*/
export function GetDataScope (params:any={}) {
return request({
url: '/api/system/role_menu_button_permission/data_scope/',
method: 'get',
params: params
})
}
/***
* 获取权限部门
* @constructor
*/
export function GetDataScopeDept (params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_to_dept_all/',
method: 'get',
params: params
})
}
/***
* 新增权限
* @param data
* @constructor
*/
export function CreatePermission(data:any) {
return request({
url: '/api/system/role_menu_button_permission/',
method: 'post',
data:data
});
}
/***
* 根据菜单获取菜单下按钮
* @param params
*/
export function getObj(params:any) {
return request({
url: '/api/system/role_menu_button_permission/menu_to_button/',
method: 'get',
params:params
});
}
/**
* 删除按钮权限
* @param data
* @constructor
*/
export function DeletePermission(data:any) {
return request({
url: `/api/system/role_menu_button_permission/${data.id}/`,
method: 'delete',
data:{}
});
}

View File

@@ -0,0 +1,379 @@
<template>
<el-drawer
size="70%"
v-model="drawer"
direction="rtl"
destroy-on-close
:before-close="handleClose"
>
<template #header>
<div>
<el-tag size="large" type="primary">当前角色:{{ editedRoleInfo.name }}</el-tag>
</div>
</template>
<div style="padding: 1em">
<div style="margin-bottom: 10px">
<el-button size="mini" type="primary" @click="onSaveAuth">保存菜单授权</el-button>
</div>
<vxe-table
ref="tableRef"
border
resizable
:row-config="{keyField: 'menu_id'}"
:tree-config="{transform: true, rowField: 'menu_id', parentField: 'parent'}"
:checkbox-config="{labelField: 'menu_id', checkRowKeys: multipleTableData,checkStrictly:true}"
:expand-config="{accordion:true}"
@toggle-row-expand="menuNodeClick"
:data="menuData">
<vxe-column type="checkbox" title="ID" width="200" tree-node></vxe-column>
<vxe-column field="name" title="目录/菜单" ></vxe-column>
<vxe-column type="expand" title="已授予权限" width="120">
<template #content="{ row, rowIndex }">
<div style="padding: 10px 0px" v-if="!row.is_catalog">
<el-button type="primary" size="small" style="margin-bottom: 0.5em"
@click="createBtnPermission">新增
</el-button>
<el-table size="small" :data="buttonPermissionData" border style="width: 100%">
<el-table-column prop="menu_button" label="权限名称" width="100">
<template #default="scope">
<div>{{ scope.row.menu_button__name }}</div>
</template>
</el-table-column>
<el-table-column prop="menu_button__value" label="权限值" width="150">
</el-table-column>
<el-table-column prop="data_range" label="权限范围" width="140">
<template #default="scope">
<div>{{ formatDataRange(scope.row.data_range) }}</div>
</template>
</el-table-column>
<el-table-column prop="dept" label="权限涉及部门"/>
<el-table-column fixed="right" label="操作" width="120">
<template #default="scope">
<el-button type="danger" size="small" @click="onDeleteBtn(scope)">删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
</vxe-column>
</vxe-table>
<!-- 弹窗-->
<el-dialog v-model="dialogFormVisible" append-to-body width="400px" title="配置按钮权限">
<el-form ref="buttonFormRef" :model="buttonForm" :rules="buttonRules" label-width="120px">
<el-form-item label="按钮" prop="menu_button">
<el-select v-model="buttonForm.menu_button" placeholder="请选择按钮" @change="onChangeButton">
<el-option v-for="(item,index) in buttonOptions" :key="index" :label="item.name"
:value="item.id"/>
</el-select>
</el-form-item>
<el-form-item label="权限范围" prop="data_range">
<el-select v-model="buttonForm.data_range" placeholder="请选择按钮">
<el-option v-for="(item,index) in dataScopeOptions" :key="index" :label="item.label"
:value="item.value"/>
</el-select>
</el-form-item>
<el-form-item label="数据部门" prop="dept" v-show="buttonForm.data_range === 4">
<div class="dept-tree">
<el-tree
:data="deptOptions"
show-checkbox
default-expand-all
:default-checked-keys="deptCheckedKeys"
ref="deptTree"
node-key="dept_id"
:check-strictly="true"
:props="{ label: 'name' }"
></el-tree>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="onSaveButtonForm">
确定
</el-button>
</span>
</template>
</el-dialog>
</div>
</el-drawer>
</template>
<script lang="ts" setup>
import {ref, defineExpose, reactive, toRefs} from 'vue'
import {ElMessageBox, ElTable} from 'element-plus'
import * as api from './api.ts'
import type {FormRules, FormInstance} from 'element-plus'
import {ElMessage} from 'element-plus'
import XEUtils from 'xe-utils'
import { VXETable, VxeTableInstance,VxeTableEvents } from 'vxe-table'
interface tableRow {
menu_id: number
name: string
}
//抽屉是否显示
const drawer = ref(false)
//当前编辑的角色信息
const editedRoleInfo = ref({})
//抽屉关闭确认
const handleClose = (done: () => void) => {
ElMessageBox.confirm('您确定要关闭?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
done()
})
.catch(() => {
// catch error
})
}
/*****菜单的配置项***/
const defaultProps = {
children: 'children',
label: 'name',
isLeaf: 'hasChild'
}
interface Tree {
name: string
children?: Tree[],
id: number
}
let menuData = ref<Tree>()
//获取菜单
const getMenuData = () => {
api.GetMenu({}).then((res: any) => {
const {data} = res
menuData.value = data
})
}
//获取已授权的菜单
const tableRef = ref<VxeTableInstance<tableRow>>()
const multipleTableData = ref()
const getRoleToMenu = () => {
api.role_to_menu({role: editedRoleInfo.value.id}).then((res: any) => {
const {data} = res
multipleTableData.value=data
})
}
let isBtnPermissionShow = ref(false)
let buttonOptions = ref<[]>()
let editedMenuInfo = ref()
//菜单节点点击事件
const menuNodeClick: VxeTableEvents.ToggleRowExpand<tableRow> = ({ expanded, row}) => {
// isBtnPermissionShow.value = !node.is_catalog
if (!row.is_catalog) {
buttonOptions.value = []
editedMenuInfo.value = row
api.GetMenuButton({menu: row.menu_id}).then((res: any) => {
const {data} = res
buttonOptions.value = data
})
api.getObj({menu: row.menu_id, role: editedRoleInfo.value.id}).then((res: any) => {
const {data} = res
buttonPermissionData.value = data
})
}
}
const menuTree = ref()
/*****菜单的配置项***/
/***按钮授权的弹窗****/
//是否显示新增表单
const dialogFormVisible = ref(false)
//部门树
const deptTree = ref()
//自定义部门数据
const deptOptions = ref()
//选中的部门数据
const deptCheckedKeys = []
//按钮表单
const buttonForm = reactive({
menu_button: null,
role: null,
menu: null,
data_range: null,
dept: []
})
//按钮表格数据
let buttonPermissionData = ref([])
//按钮表单验证
const buttonRules = reactive<FormRules>({
menu_button: [
{required: true, message: '必填项'}
],
data_range: [
{required: true, message: '必填项'}
]
})
//新增按钮
const buttonFormRef = ref<FormInstance>()
const createBtnPermission = () => {
dialogFormVisible.value = true
buttonForm.menu_button = null
buttonForm.menu = null
buttonForm.role = null
buttonForm.data_range = null
buttonForm.dept = []
}
//权限范围数据
const dataScopeOptions = ref<[]>()
//按钮值变化事件
const onChangeButton = (val: any) => {
dataScopeOptions.value = []
//获取权限值范围
api.GetDataScope({menu_button: val}).then((res: any) => {
dataScopeOptions.value = res.data
})
//获取权限部门值
api.GetDataScopeDept({menu_button: val}).then((res: any) => {
deptOptions.value = XEUtils.toArrayTree(res.data, {parentKey: 'parent', strict: false})
})
}
//过滤按钮名称
const formatMenuBtn = (val: any) => {
let obj: any = buttonOptions.value?.find((item: any) => {
return item.id === val
})
return obj ? obj.name : null
}
//过滤权限范围
const formatDataRange = (val: any) => {
let obj: any = [
{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
},
{
"value": 3,
"label": '全部数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}
].find((item: any) => {
return item.value === val
})
return obj ? obj.label : null
}
//保存按钮表单
const onSaveButtonForm = async () => {
const {id: roleId} = editedRoleInfo.value
const {id: menuId} = editedMenuInfo.value
const form: any = Object.assign({}, buttonForm)
form.role = roleId
form.menu = menuId
//选中的部门
const checkedList = deptTree.value.getCheckedKeys()
form.dept = checkedList
if (!buttonFormRef.value) return
await buttonFormRef.value.validate((valid, fields) => {
if (valid) {
api.CreatePermission(form).then((res: any) => {
const {data} = res
buttonPermissionData.value.push(data)
dialogFormVisible.value = false
ElMessage({
type: 'success',
message: res.msg,
})
})
} else {
ElMessage({
type: 'error',
title: '提交错误',
message: 'F12控制台看详情',
})
console.log('提交错误', fields)
}
})
}
//删除按钮权限
const onDeleteBtn = (scope: any) => {
const {row, $index} = scope
ElMessageBox.confirm(
'您是否要删除数据?',
'温馨提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
api.DeletePermission({id: row.id}).then((res: any) => {
buttonPermissionData.value.splice($index, 1)
ElMessage({
type: 'success',
message: res.msg,
})
})
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
/***按钮授权的弹窗****/
//初始化数据
const initGet = () => {
getMenuData()
getRoleToMenu()
}
/**
* 保存授权
*/
const onSaveAuth = () => {
const $table = tableRef.value
if ($table) {
const selectRecords = $table.getCheckboxRecords()
const menuIdList = selectRecords.map((record:any) => record.menu_id)
const {id: roleId} = editedRoleInfo.value
const data = {
role: roleId,
menu: menuIdList
}
api.SaveMenuPermission(data).then((res: any) => {
ElMessage({
message: res.msg,
type: 'success',
})
})
}
}
defineExpose({drawer, editedRoleInfo, initGet})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,238 @@
import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from '@fast-crud/fast-crud';
import _ from 'lodash-es';
import * as api from './api';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '../../../utils/message';
import {inject} from "vue";
interface CreateCrudOptionsTypes {
crudOptions: CrudOptions;
}
//此处为crudOptions配置
export const createCrudOptions = function ({ crudExpose, rolePermission }: { crudExpose: CrudExpose; rolePermission: any }): CreateCrudOptionsTypes {
const pageRequest = async (query: any) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
//权限判定
const hasPermissions = inject("$hasPermissions")
// @ts-ignore
// @ts-ignore
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show:hasPermissions('role:Update')
},
remove: {
iconRight: 'Delete',
type: 'text',
show:hasPermissions('role:Delete')
},
custom: {
text: '权限配置',
type: 'text',
show:hasPermissions('role:Update'),
tooltip: {
placement: 'top',
content: '权限配置',
},
click: (context: any): void => {
const { row } = context;
// eslint-disable-next-line no-mixed-spaces-and-tabs
rolePermission.value.drawer = true;
rolePermission.value.editedRoleInfo = row;
rolePermission.value.initGet();
},
},
},
},
form: {
col: { span: 24 },
labelWidth: '100px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
id: {
title: 'ID',
type: 'text',
column: { show: false },
search: { show: false },
form: { show: false },
},
name: {
title: '角色名称',
type: 'text',
search: { show: true },
column: {
minWidth: 120,
sortable: 'custom',
},
form: {
rules: [{ required: true, message: '角色名称必填' }],
component: {
placeholder: '请输入角色名称',
},
},
},
key: {
title: '权限标识',
type: 'text',
search: { show: false },
column: {
width: 120,
sortable: 'custom',
},
form: {
rules: [{ required: true, message: '权限标识必填' }],
placeholder: '输入权限标识',
},
},
sort: {
title: '排序',
search: { show: false },
type: 'number',
column: {
width: 90,
sortable: 'custom',
},
form: {
rules: [{ required: true, message: '排序必填' }],
value: 1,
},
},
admin: {
title: '是否管理员',
search: { show: false },
type: 'dict-radio',
dict: dict({
data: [
{
label: '是',
value: true,
color: 'success',
},
{
label: '否',
value: false,
color: 'danger',
},
],
}),
column: {
width: 130,
sortable: 'custom',
},
form: {
rules: [{ required: true, message: '是否管理员必填' }],
value: false,
},
},
status: {
title: '状态',
search: { show: true },
type: 'dict-radio',
column: {
width:100,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
onChange: compute((context) => {
return () => {
api.UpdateObj(context.row).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
};
}),
},
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
update_datetime: {
title: '更新时间',
type: 'text',
search: { show: false },
column: {
width: 170,
sortable: 'custom',
},
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
create_datetime: {
title: '创建时间',
type: 'text',
search: { show: false },
column: {
sortable: 'custom',
width: 170,
},
form: {
show: false,
component: {
placeholder: '输入关键词搜索',
},
},
},
// description: {
// title: '备注',
// type: 'textarea',
// search: {show: false},
// form: {
// component: {
// maxlength: 200,
// placeholder: '输入备注',
// },
// },
// },
},
},
};
};

View File

@@ -0,0 +1,40 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #cell_url="scope">
<el-tag size="small">{{ scope.row.url }}</el-tag>
</template>
</fs-crud>
<!-- <RolePermission ref="rolePermission"></RolePermission>-->
<permission ref="rolePermission"></permission>
</fs-page>
</template>
<script lang="ts" setup name="role">
import { ref, onMounted } from 'vue';
import { useExpose, useCrud, dict } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import RolePermission from '/@/views/system/rolePermission/index.vue';
import permission from './components/permission.vue'
import * as api from './api';
import _ from 'lodash-es';
const rolePermission = ref();
defineExpose(rolePermission);
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose, rolePermission });
// 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
// 你可以调用此方法重新初始化crud配置
// resetCrudOptions(options)
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>

View File

@@ -0,0 +1,107 @@
import { request } from "/@/utils/service";
/**
* 获取角色所拥有的菜单
* @param params
*/
export function GetMenu(params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_get_menu/',
method: 'get',
params:params
});
}
/***
* 新增权限
* @param data
* @constructor
*/
export function SaveMenuPermission(data:any) {
return request({
url: '/api/system/role_menu_permission/save_auth/',
method: 'post',
data:data
});
}
/**
* 获取菜单下的按钮
* @param params
* @constructor
*/
export function GetMenuButton(params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_menu_get_button/',
method: 'get',
params:params
});
}
/***
* 根据角色获取数据权限范围
* @constructor
*/
export function GetDataScope (params:any={}) {
return request({
url: '/api/system/role_menu_button_permission/data_scope/',
method: 'get',
params: params
})
}
/***
* 获取权限部门
* @constructor
*/
export function GetDataScopeDept (params:any) {
return request({
url: '/api/system/role_menu_button_permission/role_to_dept_all/',
method: 'get',
params: params
})
}
/***
* 新增权限
* @param data
* @constructor
*/
export function CreatePermission(data:any) {
return request({
url: '/api/system/role_menu_button_permission/',
method: 'post',
data:data
});
}
/***
* 根据菜单获取菜单下按钮
* @param params
*/
export function getObj(params:any) {
return request({
url: '/api/system/role_menu_button_permission/menu_to_button/',
method: 'get',
params:params
});
}
/**
* 删除按钮权限
* @param data
* @constructor
*/
export function DeletePermission(data:any) {
return request({
url: `/api/system/role_menu_button_permission/${data.id}/`,
method: 'delete',
data:{}
});
}

View File

@@ -0,0 +1,432 @@
<template>
<el-drawer
size="70%"
v-model="drawer"
direction="rtl"
destroy-on-close
:before-close="handleClose"
>
<template #header>
<div>
<el-tag size="large" type="primary">当前角色:{{ editedRoleInfo.name }}</el-tag>
</div>
</template>
<div style="padding: 1em">
<el-row :gutter="10">
<el-col :xs="24" :sm="24" :md="8" :lg="6" :xl="6">
<el-card header="菜单页面授权">
<template #header>
<div class="card-header">
<el-tooltip effect="dark" content="点击菜单项,可对菜单下的按钮/接口授权"
placement="right">
<div>
<span>菜单页面</span>
<el-icon>
<QuestionFilled/>
</el-icon>
</div>
</el-tooltip>
<el-button size="mini" type="primary" @click="onSaveAuth">保存菜单授权</el-button>
</div>
</template>
<el-tree :data="menuData"
ref="menuTree"
show-checkbox
node-key="id"
highlight-current
:expand-on-click-node="false"
:check-on-click-node="true"
:props="defaultProps"
@node-click="menuNodeClick"
/>
</el-card>
</el-col>
<el-col :xs="24" :sm="24" :md="16" :lg="18" :xl="18">
<!-- <el-alert title="对页面菜单下按钮授权" description="新增或删除对菜单下的按钮/接口授权" type="warning" />-->
<el-card v-if="isBtnPermissionShow">
<template #header>
<div class="card-header">
<el-tooltip effect="dark" content="新增或删除对菜单下的按钮/接口授权" placement="right">
<div>
<span>按钮/接口授权</span>
<el-icon>
<QuestionFilled/>
</el-icon>
</div>
</el-tooltip>
</div>
</template>
<div>
<el-divider content-position="left">{{ editedMenuInfo.name }}</el-divider>
<el-button type="primary" size="small" style="margin-bottom: 0.5em"
@click="createBtnPermission">新增
</el-button>
<el-table size="small" :data="buttonPermissionData" border style="width: 100%">
<el-table-column prop="menu_button" label="权限名称" width="100">
<template #default="scope">
<div>{{ formatMenuBtn(scope.row.menu_button) }}</div>
</template>
</el-table-column>
<el-table-column prop="data_range" label="权限范围" width="140">
<template #default="scope">
<div>{{ formatDataRange(scope.row.data_range) }}</div>
</template>
</el-table-column>
<el-table-column prop="dept" label="权限涉及部门"/>
<el-table-column fixed="right" label="操作" width="120">
<template #default="scope">
<el-button type="danger" size="small" @click="onDeleteBtn(scope)">删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- <el-divider content-position="left">字段授权</el-divider>-->
<!-- <el-table size="small" :data="crudPermissionData" border style="width: 100%">-->
<!-- <el-table-column prop="field" label="字段"></el-table-column>-->
<!-- <el-table-column prop="table" label="列表显示">-->
<!-- <template #default="scope">-->
<!-- <div>-->
<!-- <el-switch size="mini" v-model="scope.row.table"/>-->
<!-- </div>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column prop="view" label="表单查看">-->
<!-- <template #default="scope">-->
<!-- <div>-->
<!-- <el-switch size="mini" v-model="scope.row.view"/>-->
<!-- </div>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column prop="edit" label="表单编辑">-->
<!-- <template #default="scope">-->
<!-- <div>-->
<!-- <el-switch size="mini" v-model="scope.row.edit"/>-->
<!-- </div>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
</el-card>
</el-col>
</el-row>
<el-dialog v-model="dialogFormVisible" width="400px" title="配置按钮权限">
<el-form ref="buttonFormRef" :model="buttonForm" :rules="buttonRules" label-width="120px">
<el-form-item label="按钮" prop="menu_button">
<el-select v-model="buttonForm.menu_button" placeholder="请选择按钮" @change="onChangeButton">
<el-option v-for="(item,index) in buttonOptions" :key="index" :label="item.name"
:value="item.id"/>
</el-select>
</el-form-item>
<el-form-item label="权限范围" prop="data_range">
<el-select v-model="buttonForm.data_range" placeholder="请选择按钮">
<el-option v-for="(item,index) in dataScopeOptions" :key="index" :label="item.label"
:value="item.value"/>
</el-select>
</el-form-item>
<el-form-item label="数据部门" prop="dept" v-show="buttonForm.data_range === 4">
<div class="dept-tree">
<el-tree
:data="deptOptions"
show-checkbox
default-expand-all
:default-checked-keys="deptCheckedKeys"
ref="deptTree"
node-key="id"
:check-strictly="true"
:props="{ label: 'name' }"
></el-tree>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="onSaveButtonForm">
确定
</el-button>
</span>
</template>
</el-dialog>
</div>
</el-drawer>
</template>
<script lang="ts" setup name="rolePermission">
import {ref, defineExpose, reactive, toRefs} from 'vue'
import {ElMessageBox} from 'element-plus'
import * as api from './api'
import type {FormRules, FormInstance} from 'element-plus'
import {ElMessage} from 'element-plus'
import XEUtils from 'xe-utils'
//抽屉是否显示
const drawer = ref(false)
//当前编辑的角色信息
const editedRoleInfo = ref({})
//抽屉关闭确认
const handleClose = (done: () => void) => {
ElMessageBox.confirm('您确定要关闭?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
done()
})
.catch(() => {
// catch error
})
}
/*****菜单的配置项***/
const defaultProps = {
children: 'children',
label: 'name',
isLeaf: 'hasChild'
}
interface Tree {
name: string
children?: Tree[],
id: number
}
let menuData = ref<Tree>()
//获取菜单
const getMenuData = () => {
api.GetMenu({}).then((res: any) => {
const {data} = res
const list = XEUtils.toArrayTree(data, {parentKey: "parent", key:'menu_id',strict: true})
menuData.value = list
})
}
let isBtnPermissionShow = ref(false)
let buttonOptions = ref<[]>()
let editedMenuInfo = ref()
//菜单节点点击事件
const menuNodeClick = (node: any, obj: any) => {
isBtnPermissionShow.value = !node.is_catalog
if (!node.is_catalog) {
buttonOptions.value = []
editedMenuInfo.value = node
api.GetMenuButton({menu: node.menu_id}).then((res: any) => {
const {data} = res
buttonOptions.value = data
})
api.getObj({menu: node.menu_id, role: editedRoleInfo.value.id}).then((res: any) => {
const {data} = res
buttonPermissionData.value = data
})
}
}
const menuTree = ref()
/*****菜单的配置项***/
/***按钮授权的弹窗****/
//是否显示新增表单
const dialogFormVisible = ref(false)
//部门树
const deptTree = ref()
//自定义部门数据
const deptOptions = ref()
//选中的部门数据
const deptCheckedKeys = []
//按钮表单
const buttonForm = reactive({
menu_button: null,
role: null,
menu: null,
data_range: null,
dept: []
})
//按钮表格数据
let buttonPermissionData = ref([])
//按钮表单验证
const buttonRules = reactive<FormRules>({
menu_button: [
{required: true, message: '必填项'}
],
data_range: [
{required: true, message: '必填项'}
]
})
//新增按钮
const buttonFormRef = ref<FormInstance>()
const createBtnPermission = () => {
dialogFormVisible.value = true
buttonForm.menu_button = null
buttonForm.menu = null
buttonForm.role = null
buttonForm.data_range = null
buttonForm.dept = []
}
//权限范围数据
const dataScopeOptions = ref<[]>()
//按钮值变化事件
const onChangeButton = (val: any) => {
dataScopeOptions.value = []
//获取权限值范围
api.GetDataScope({menu_button: val}).then((res: any) => {
dataScopeOptions.value = res.data
})
//获取权限部门值
api.GetDataScopeDept({menu_button: val}).then((res: any) => {
deptOptions.value = XEUtils.toArrayTree(res.data, {parentKey: 'parent', strict: false})
})
}
//过滤按钮名称
const formatMenuBtn = (val: any) => {
let obj: any = buttonOptions.value?.find((item: any) => {
return item.id === val
})
return obj ? obj.name : null
}
//过滤权限范围
const formatDataRange = (val: any) => {
let obj: any = [
{
"value": 0,
"label": '仅本人数据权限'
},
{
"value": 1,
"label": '本部门及以下数据权限'
},
{
"value": 2,
"label": '本部门数据权限'
},
{
"value": 3,
"label": '全部数据权限'
},
{
"value": 4,
"label": '自定义数据权限'
}
].find((item: any) => {
return item.value === val
})
return obj ? obj.label : null
}
//保存按钮表单
const onSaveButtonForm = async () => {
const {id: roleId} = editedRoleInfo.value
const {id: menuId} = editedMenuInfo.value
const form: any = Object.assign({}, buttonForm)
form.role = roleId
form.menu = menuId
//选中的部门
const checkedList = deptTree.value.getCheckedKeys()
form.dept = checkedList
if (!buttonFormRef.value) return
await buttonFormRef.value.validate((valid, fields) => {
if (valid) {
api.CreatePermission(form).then((res: any) => {
const {data} = res
buttonPermissionData.value.push(data)
dialogFormVisible.value = false
ElMessage({
type: 'success',
message: res.msg,
})
})
} else {
ElMessage({
type: 'error',
title: '提交错误',
message: 'F12控制台看详情',
})
console.log('提交错误', fields)
}
})
}
//删除按钮权限
const onDeleteBtn = (scope: any) => {
const {row, $index} = scope
ElMessageBox.confirm(
'您是否要删除数据?',
'温馨提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
api.DeletePermission({id: row.id}).then((res: any) => {
buttonPermissionData.value.splice($index, 1)
ElMessage({
type: 'success',
message: res.msg,
})
})
})
.catch(() => {
ElMessage({
type: 'info',
message: '取消删除',
})
})
}
/***按钮授权的弹窗****/
//初始化数据
const initGet = () => {
getMenuData()
}
/**
* 保存授权
*/
const onSaveAuth = () => {
//选中的菜单
const checkedList = menuTree.value.getCheckedKeys()
//半选中的菜单
const halfCheckedList = menuTree.value.getHalfCheckedKeys()
//合并的菜单数据
const menuIdList = [...checkedList, ...halfCheckedList]
// console.log(menuIdList)
const {id: roleId} = editedRoleInfo.value
const data = {
role: roleId,
menu: menuIdList
}
api.SaveMenuPermission(data).then((res: any) => {
ElMessage({
message: res.msg,
type: 'success',
})
})
}
defineExpose({drawer, editedRoleInfo, initGet})
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.dept-tree::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
.dept-tree {
height: 160px;
overflow-y: scroll;
scrollbar-width: none; /* firefox */
-ms-overflow-style: none; /* IE 10+ */
border: 1px solid #e1e1e1;
width: 16em;
border-radius: 2px;
}
</style>

View File

@@ -0,0 +1,58 @@
import { request,downloadFile } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/user/';
export function GetDept(query: PageQuery) {
return request({
url: "/api/system/dept/dept_lazy_tree/",
method: 'get',
params: query,
});
}
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}
export function exportData(params:any){
return downloadFile({
url: apiPrefix + 'export_data/',
params: params,
method: 'get'
})
}

View File

@@ -0,0 +1,373 @@
import * as api from './api';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '/@/utils/message';
import { inject } from 'vue';
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
const exportRequest = async (query: UserPageQuery) => {
return await api.exportData(query)
}
//权限判定
const hasPermissions:any = inject('$hasPermissions');
return {
crudOptions: {
table: {
remove: {
confirmMessage: '是否删除该用户?',
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
show: hasPermissions('user:Create')
// show:true
},
export:{
text:"导出",//按钮文字
title:"导出",//鼠标停留显示的信息
click(){
return exportRequest(crudExpose!.getSearchFormData())
}
}
}
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 200,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show: hasPermissions('user:Update'),
},
remove: {
iconRight: 'Delete',
type: 'text',
show: hasPermissions('user:Delete'),
},
custom: {
text: '重设密码',
type: 'text',
show: hasPermissions('user:ResetPassword'),
tooltip: {
placement: 'top',
content: '重设密码',
},
//@ts-ignore
click: (ctx: any) => {
const { row } = ctx;
},
},
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
username: {
title: '账号',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '账号必填项',
},
],
component: {
placeholder: '请输入账号',
},
},
},
password: {
title: '密码',
type: 'input',
column: {
show: false,
},
editForm: {
show: false,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '密码必填项',
},
],
component: {
span: 12,
showPassword: true,
placeholder: '请输入密码',
},
// value: vm.systemConfig('base.default_password'),
},
/* valueResolve(row, key) {
if (row.password) {
row.password = vm.$md5(row.password)
}
} */
},
name: {
title: '姓名',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '姓名必填项',
},
],
component: {
span: 12,
placeholder: '请输入姓名',
},
},
},
dept: {
title: '部门',
search: {
disabled: true,
},
type: 'dict-tree',
dict: dict({
isTree: true,
url: '/api/system/dept/all_dept/',
value: 'id',
label: 'name',
getData: async ({ url }: { url: string }) => {
return request({
url: url,
}).then((ret: any) => {
return ret.data;
});
},
}),
column: {
minWidth: 150, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
filterable: true,
placeholder: '请选择',
props: {
props: {
value: 'id',
label: 'name',
},
},
},
},
},
role: {
title: '角色',
search: {
disabled: true,
},
type: 'dict-select',
dict: dict({
url: '/api/system/role/',
value: 'id',
label: 'name',
isTree: true,
getData: async ({ url }: { url: string }) => {
return request({
url: url,
params: {
page: 1,
limit: 10,
},
}).then((ret: any) => {
return ret.data;
});
},
}),
column: {
minWidth: 100, //最小列宽
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
multiple: true,
filterable: true,
placeholder: '请选择角色',
},
},
},
mobile: {
title: '手机号码',
search: {
show: true,
},
type: 'input',
column: {
minWidth: 120, //最小列宽
},
form: {
rules: [
{
max: 20,
message: '请输入正确的手机号码',
trigger: 'blur',
},
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号码',
},
],
component: {
placeholder: '请输入手机号码',
},
},
},
email: {
title: '邮箱',
column: {
width: 260,
},
form: {
rules: [
{
type: 'email',
message: '请输入正确的邮箱地址',
trigger: ['blur', 'change'],
},
],
component: {
placeholder: '请输入邮箱',
},
},
},
gender: {
title: '性别',
type: 'dict-select',
dict: dict({
data: dictionary('gender'),
}),
form: {
value: 1,
component: {
span: 12,
},
},
component: { props: { color: 'auto' } }, // 自动染色
},
user_type: {
title: '用户类型',
search: {
show: true,
},
type: 'dict-select',
dict: dict({
data: dictionary('user_type'),
}),
column: {
minWidth: 100, //最小列宽
},
form: {
show: false,
value: 0,
component: {
span: 12,
},
},
},
is_active: {
title: '锁定',
search: {
show: true,
},
type: 'dict-radio',
column: {
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
onChange: compute((context) => {
return () => {
api.UpdateObj(context.row).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
};
}),
},
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
avatar: {
title: '头像',
type: 'avatar-cropper',
form: {
show: false,
},
},
},
},
};
};

View File

@@ -0,0 +1,148 @@
<template>
<fs-page>
<el-row class="mx-2">
<el-col xs="24" :sm="8" :md="6" :lg="4" :xl="4" class="p-1">
<el-card :body-style="{ height: '100%' }">
<p class="font-mono font-black text-center text-xl pb-5">
部门列表
<el-tooltip effect="dark" :content="content" placement="right">
<el-icon>
<QuestionFilled />
</el-icon>
</el-tooltip>
</p>
<el-input v-model="filterText" :placeholder="placeholder" />
<el-tree
ref="treeRef"
class="font-mono font-bold leading-6 text-7xl"
:data="data"
:props="treeProps"
:filter-node-method="filterNode"
icon="ArrowRightBold"
:indent="12"
@node-click="onTreeNodeClick"
>
<template #default="{ node, data }">
<span class="text-center font-black font-normal">{{ node.label }}</span>
</template>
</el-tree>
</el-card>
</el-col>
<el-col xs="24" :sm="16" :md="18" :lg="20" :xl="20" class="p-1">
<el-card :body-style="{ height: '100%' }">
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #actionbar-right>
<importExcel api="api/system/user/" >导入 </importExcel>
</template>
</fs-crud>
</el-card>
</el-col>
</el-row>
</fs-page>
</template>
<script lang="ts" setup name="user">
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import * as api from './api';
import { ElTree } from 'element-plus';
import { ref, onMounted, watch, toRaw } from 'vue';
import XEUtils from 'xe-utils';
import importExcel from '/@/components/importExcel/index.vue'
interface Tree {
id: number;
name: string;
status: boolean;
children?: Tree[];
}
interface APIResponseData {
code?: number;
data: [];
msg?: string;
}
// 引入组件
const placeholder = ref('请输入部门名称');
const filterText = ref('');
const treeRef = ref<InstanceType<typeof ElTree>>();
const treeProps = {
children: 'children',
label: 'name',
icon: 'icon',
};
watch(filterText, (val) => {
treeRef.value!.filter(val);
});
const filterNode = (value: string, data: Tree) => {
if (!value) return true;
return toRaw(data).name.indexOf(value) !== -1;
};
let data = ref([]);
const content = `
1.部门信息;
`;
const getData = () => {
api.GetDept({}).then((ret: APIResponseData) => {
const responseData = ret.data;
const result = XEUtils.toArrayTree(responseData, {
parentKey: 'parent',
children: 'children',
strict: true,
});
data.value = result;
});
};
//树形点击事件
const onTreeNodeClick = (node: any) => {
const { id } = node;
crudExpose.doSearch({ form: { dept: id } });
};
// 页面打开后获取列表数据
onMounted(() => {
getData();
});
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
// 暴露的方法
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
// 初始化crud配置
const { resetCrudOptions } = useCrud({ crudExpose, crudOptions });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>
<style lang="scss" scoped>
.el-row {
height: 100%;
.el-col {
height: 100%;
}
}
.el-card {
height: 100%;
}
.font-normal {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
}
</style>

View File

@@ -0,0 +1,41 @@
import { request } from '/@/utils/service';
import { UserPageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/api_white_list/';
export function GetList(query: UserPageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,237 @@
import * as api from './api';
import { dict, UserPageQuery, AddReq, DelReq, EditReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet } from '@fast-crud/fast-crud';
import { request } from '/@/utils/service';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '/@/utils/message';
import {inject} from "vue";
export const createCrudOptions = function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
//权限判定
const hasPermissions = inject("$hasPermissions")
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 150,
buttons: {
view: {
show: false,
},
edit: {
iconRight: 'Edit',
type: 'text',
show:hasPermissions("api_white_list:Update")
},
remove: {
iconRight: 'Delete',
type: 'text',
show:hasPermissions("api_white_list:Delete")
},
},
},
form: {
col: { span: 24 },
labelWidth: '110px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
//type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
//@ts-ignore
formatter: (context) => {
//计算序号,你可以自定义计算规则,此处为翻页累加
let index = context.index ?? 1;
let pagination: any = crudExpose!.crudBinding.value.pagination;
return ((pagination.currentPage ?? 1) - 1) * pagination.pageSize + index + 1;
},
},
},
search: {
title: '关键词',
column: {
show: false,
},
search: {
show: true,
component: {
props: {
clearable: true,
},
placeholder: '请输入关键词',
},
},
form: {
show: false,
component: {
props: {
clearable: true,
},
},
},
},
method: {
title: '请求方式',
sortable: 'custom',
search: {
disabled: false,
},
type: 'dict-select',
dict: dict({
data: [
{
label: 'GET',
value: 0,
},
{
label: 'POST',
value: 1,
},
{
label: 'PUT',
value: 2,
},
{
label: 'DELETE',
value: 3,
},
{
label: 'PATCH',
value: 4,
},
],
}),
column:{
minWidth: 120,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 12,
},
itemProps: {
class: { yxtInput: true },
},
},
},
url: {
title: '接口地址',
sortable: 'custom',
search: {
disabled: true,
},
type: 'dict-select',
dict: dict({
async getData(dict: any) {
return request('/swagger.json').then((ret: any) => {
const res = Object.keys(ret.paths);
const data = [];
for (const item of res) {
const obj = { label: '', value: '' };
obj.label = item;
obj.value = item;
data.push(obj);
}
return data;
});
},
}),
column:{
minWidth: 200,
},
form: {
rules: [
// 表单校验规则
{
required: true,
message: '必填项',
},
],
component: {
span: 24,
props: {
elProps: {
allowCreate: true,
filterable: true,
clearable: true,
},
},
},
itemProps: {
class: { yxtInput: true },
},
helper: {
position: 'label',
tooltip: {
placement: 'top-start',
},
text: '请正确填写,以免请求时被拦截。匹配单例使用正则,例如:/api/xx/.*?/',
},
},
},
enable_datasource: {
title: '数据权限认证',
search: {
disabled: false,
},
type: 'dict-radio',
column: {
minWidth:120,
component: {
name: 'fs-dict-switch',
activeText: '',
inactiveText: '',
style: '--el-switch-on-color: var(--el-color-primary); --el-switch-off-color: #dcdfe6',
onChange: compute((context) => {
return () => {
api.UpdateObj(context.row).then((res: APIResponseData) => {
successMessage(res.msg as string);
});
};
}),
},
},
dict: dict({
data: dictionary('button_status_bool'),
}),
},
},
},
};
};

View File

@@ -0,0 +1,18 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
</fs-page>
</template>
<script lang="ts" setup name="whiteList">
import {ref, onMounted} from 'vue';
import {useFs} from '@fast-crud/fast-crud';
import {createCrudOptions} from './crud';
const {crudBinding, crudRef, crudExpose} = useFs({createCrudOptions});
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>