项目初始化

This commit is contained in:
2026-03-18 15:54:43 +08:00
commit dfcd762e23
601 changed files with 57883 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
<template>
<div class="default-main ba-table-box">
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<!-- 表格顶部菜单 -->
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('auth.admin.username') + '/' + t('auth.admin.nickname') })"
/>
<!-- 表格 -->
<!-- 要使用`el-table`组件原有的属性直接加在Table标签上即可 -->
<Table />
<!-- 表单 -->
<PopupForm />
</div>
</template>
<script setup lang="ts">
import { provide } from 'vue'
import baTableClass from '/@/utils/baTable'
import PopupForm from './popupForm.vue'
import Table from '/@/components/table/index.vue'
import TableHeader from '/@/components/table/header/index.vue'
import { defaultOptButtons } from '/@/components/table'
import { baTableApi } from '/@/api/common'
import { useAdminInfo } from '/@/stores/adminInfo'
import { useI18n } from 'vue-i18n'
defineOptions({
name: 'auth/admin',
})
const { t } = useI18n()
const adminInfo = useAdminInfo()
const optButtons = defaultOptButtons(['edit', 'delete'])
optButtons[1].display = (row) => {
return row.id != adminInfo.id
}
const baTable = new baTableClass(
new baTableApi('/admin/auth.Admin/'),
{
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('Id'), prop: 'id', align: 'center', operator: '=', operatorPlaceholder: t('Id'), width: 70 },
{ label: t('auth.admin.username'), prop: 'username', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('auth.admin.nickname'), prop: 'nickname', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('auth.admin.group'), prop: 'group_name_arr', align: 'center', operator: false, render: 'tags' },
{ label: t('auth.admin.avatar'), prop: 'avatar', align: 'center', render: 'image', operator: false },
{ label: t('auth.admin.email'), prop: 'email', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{ label: t('auth.admin.mobile'), prop: 'mobile', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{
label: t('auth.admin.Last login'),
prop: 'last_login_time',
align: 'center',
render: 'datetime',
sortable: 'custom',
operator: 'RANGE',
width: 160,
},
{ label: t('Create time'), prop: 'create_time', align: 'center', render: 'datetime', sortable: 'custom', operator: 'RANGE', width: 160 },
{
label: t('State'),
prop: 'status',
align: 'center',
render: 'tag',
custom: { disable: 'danger', enable: 'success' },
replaceValue: { disable: t('Disable'), enable: t('Enable') },
},
{
label: t('Operate'),
align: 'center',
width: '100',
render: 'buttons',
buttons: optButtons,
operator: false,
},
],
dblClickNotEditColumn: [undefined, 'status'],
},
{
defaultItems: {
status: 'enable',
},
}
)
provide('baTable', baTable)
baTable.mount()
baTable.getData()
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,198 @@
<template>
<!-- 对话框表单 -->
<el-dialog
class="ba-operate-dialog"
:close-on-click-modal="false"
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
@close="baTable.toggleForm"
:destroy-on-close="true"
>
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
</div>
</template>
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
<div
class="ba-operate-form"
:class="'ba-' + baTable.form.operate + '-form'"
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
>
<el-form
ref="formRef"
@keyup.enter="baTable.onSubmit(formRef)"
:model="baTable.form.items"
:label-position="config.layout.shrink ? 'top' : 'right'"
:label-width="baTable.form.labelWidth + 'px'"
:rules="rules"
v-if="!baTable.form.loading"
>
<FormItem
:label="t('auth.admin.username')"
v-model="baTable.form.items!.username"
type="string"
prop="username"
:placeholder="t('auth.admin.Administrator login')"
/>
<FormItem
:label="t('auth.admin.nickname')"
v-model="baTable.form.items!.nickname"
type="string"
prop="nickname"
:placeholder="t('Please input field', { field: t('auth.admin.nickname') })"
/>
<FormItem
:label="t('auth.admin.group')"
v-model="baTable.form.items!.group_arr"
prop="group_arr"
type="remoteSelect"
:key="'group-' + baTable.form.items!.id"
:input-attr="{
multiple: true,
params: { isTree: true, absoluteAuth: adminInfo.id == baTable.form.items!.id ? 0 : 1 },
field: 'name',
remoteUrl: '/admin/auth.Group/index',
placeholder: t('Click select'),
}"
/>
<FormItem :label="t('auth.admin.avatar')" type="image" v-model="baTable.form.items!.avatar" />
<FormItem
:label="t('auth.admin.email')"
prop="email"
v-model="baTable.form.items!.email"
type="string"
:placeholder="t('Please input field', { field: t('auth.admin.email') })"
/>
<FormItem
:label="t('auth.admin.mobile')"
prop="mobile"
v-model="baTable.form.items!.mobile"
type="string"
:placeholder="t('Please input field', { field: t('auth.admin.mobile') })"
/>
<FormItem
:label="t('auth.admin.Password')"
prop="password"
v-model="baTable.form.items!.password"
type="password"
:input-attr="{ autocomplete: 'new-password' }"
:placeholder="
baTable.form.operate == 'Add'
? t('Please input field', { field: t('auth.admin.Password') })
: t('auth.admin.Please leave blank if not modified')
"
/>
<el-form-item prop="motto" :label="t('auth.admin.Personal signature')">
<el-input
@keyup.enter.stop=""
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
v-model="baTable.form.items!.motto"
type="textarea"
:placeholder="t('Please input field', { field: t('auth.admin.Personal signature') })"
></el-input>
</el-form-item>
<FormItem
:label="t('State')"
v-model="baTable.form.items!.status"
type="radio"
:input-attr="{
border: true,
content: { disable: t('Disable'), enable: t('Enable') },
}"
/>
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
<el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, inject, watch, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import type baTableClass from '/@/utils/baTable'
import { regularPassword, buildValidatorData } from '/@/utils/validate'
import type { FormItemRule } from 'element-plus'
import FormItem from '/@/components/formItem/index.vue'
import { useAdminInfo } from '/@/stores/adminInfo'
import { useConfig } from '/@/stores/config'
const config = useConfig()
const adminInfo = useAdminInfo()
const formRef = useTemplateRef('formRef')
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
username: [buildValidatorData({ name: 'required', title: t('auth.admin.username') }), buildValidatorData({ name: 'account' })],
nickname: [buildValidatorData({ name: 'required', title: t('auth.admin.nickname') })],
group_arr: [buildValidatorData({ name: 'required', message: t('Please select field', { field: t('auth.admin.group') }) })],
email: [buildValidatorData({ name: 'email', message: t('Please enter the correct field', { field: t('auth.admin.email') }) })],
mobile: [buildValidatorData({ name: 'mobile', message: t('Please enter the correct field', { field: t('auth.admin.mobile') }) })],
password: [
{
validator: (rule: any, val: string, callback: Function) => {
if (baTable.form.operate == 'Add') {
if (!val) {
return callback(new Error(t('Please input field', { field: t('auth.admin.Password') })))
}
} else {
if (!val) {
return callback()
}
}
if (!regularPassword(val)) {
return callback(new Error(t('validate.Please enter the correct password')))
}
return callback()
},
trigger: 'blur',
},
],
})
watch(
() => baTable.form.operate,
(newVal) => {
rules.password![0].required = newVal == 'Add'
}
)
</script>
<style scoped lang="scss">
.avatar-uploader {
display: flex;
align-items: center;
justify-content: center;
position: relative;
border-radius: var(--el-border-radius-small);
box-shadow: var(--el-box-shadow-light);
border: 1px dashed var(--el-border-color);
cursor: pointer;
overflow: hidden;
width: 110px;
height: 110px;
}
.avatar-uploader:hover {
border-color: var(--el-color-primary);
}
.avatar {
width: 110px;
height: 110px;
display: block;
}
.image-slot {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<div class="default-main ba-table-box">
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<!-- 表格顶部菜单 -->
<TableHeader
:buttons="['refresh', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('auth.adminLog.title') })"
/>
<!-- 表格 -->
<!-- 要使用`el-table`组件原有的属性直接加在Table标签上即可 -->
<Table />
<Info />
</div>
</template>
<script setup lang="ts">
import { concat, cloneDeep } from 'lodash-es'
import { provide } from 'vue'
import baTableClass from '/@/utils/baTable'
import Table from '/@/components/table/index.vue'
import TableHeader from '/@/components/table/header/index.vue'
import { defaultOptButtons } from '/@/components/table'
import { baTableApi } from '/@/api/common'
import { useI18n } from 'vue-i18n'
import Info from './info.vue'
import { buildJsonToElTreeData } from '/@/utils/common'
defineOptions({
name: 'auth/adminLog',
})
const { t } = useI18n()
let optButtons: OptButton[] = [
{
render: 'tipButton',
name: 'info',
title: 'Info',
text: '',
type: 'primary',
icon: 'fa fa-search-plus',
class: 'table-row-edit',
disabledTip: false,
click: (row: TableRow) => {
infoButtonClick(row)
},
},
]
optButtons = concat(optButtons, defaultOptButtons(['delete']))
const baTable = new baTableClass(new baTableApi('/admin/auth.AdminLog/'), {
column: [
{ type: 'selection', align: 'center', operator: false },
{ label: t('Id'), prop: 'id', align: 'center', operator: '=', operatorPlaceholder: t('Id'), width: 70 },
{
label: t('auth.adminLog.admin_id'),
prop: 'admin_id',
align: 'center',
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
width: 70,
},
{
label: t('auth.adminLog.username'),
prop: 'username',
align: 'center',
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
width: 160,
},
{ label: t('auth.adminLog.title'), prop: 'title', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query') },
{
show: false,
label: t('auth.adminLog.data'),
prop: 'data',
align: 'center',
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
showOverflowTooltip: true,
},
{
label: t('auth.adminLog.url'),
prop: 'url',
align: 'center',
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
showOverflowTooltip: true,
render: 'url',
},
{ label: t('auth.adminLog.ip'), prop: 'ip', align: 'center', operator: 'LIKE', operatorPlaceholder: t('Fuzzy query'), render: 'tag' },
{
label: t('auth.adminLog.useragent'),
prop: 'useragent',
align: 'center',
operator: 'LIKE',
operatorPlaceholder: t('Fuzzy query'),
showOverflowTooltip: true,
},
{
label: t('Create time'),
prop: 'create_time',
align: 'center',
render: 'datetime',
sortable: 'custom',
operator: 'RANGE',
width: 160,
},
{
label: t('Operate'),
align: 'center',
width: '100',
render: 'buttons',
buttons: optButtons,
operator: false,
},
],
dblClickNotEditColumn: [undefined],
})
// 利用双击单元格前钩子重写双击操作
baTable.before.onTableDblclick = ({ row }) => {
infoButtonClick(row)
return false
}
baTable.mount()
baTable.getData()
provide('baTable', baTable)
/** 点击查看详情按钮响应 */
const infoButtonClick = (row: TableRow) => {
if (!row) return
// 数据来自表格数据,未重新请求api,深克隆,不然可能会影响表格
let rowClone = cloneDeep(row)
rowClone.data = rowClone.data ? [{ label: '点击展开', children: buildJsonToElTreeData(JSON.parse(rowClone.data)) }] : []
baTable.form.extend!['info'] = rowClone
baTable.form.operate = 'Info'
}
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,62 @@
<template>
<!-- 查看详情 -->
<el-dialog class="ba-operate-dialog" :model-value="baTable.form.operate ? true : false" @close="baTable.toggleForm">
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">{{ t('Info') }}</div>
</template>
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
<div class="ba-operate-form" :class="'ba-' + baTable.form.operate + '-form'">
<el-descriptions :column="2" border>
<el-descriptions-item :label="t('Id')">
{{ baTable.form.extend!.info.id }}
</el-descriptions-item>
<el-descriptions-item :label="t('auth.adminLog.Operation administrator')">
{{ baTable.form.extend!.info.username }}
</el-descriptions-item>
<el-descriptions-item :label="t('auth.adminLog.title')">
{{ baTable.form.extend!.info.title }}
</el-descriptions-item>
<el-descriptions-item :label="t('auth.adminLog.Operator IP')">
{{ baTable.form.extend!.info.ip }}
</el-descriptions-item>
<el-descriptions-item :width="120" :span="2" label="URL">
{{ baTable.form.extend!.info.url }}
</el-descriptions-item>
<el-descriptions-item :width="120" :span="2" label="User Agent">
{{ baTable.form.extend!.info.useragent }}
</el-descriptions-item>
<el-descriptions-item :width="120" :span="2" :label="t('Create time')">
{{ timeFormat(baTable.form.extend!.info.create_time) }}
</el-descriptions-item>
<el-descriptions-item :width="120" :span="2" :label="t('auth.adminLog.Request data')">
<el-tree class="table-el-tree" :data="baTable.form.extend!.info.data" :props="{ label: 'label', children: 'children' }" />
</el-descriptions-item>
</el-descriptions>
</div>
</el-scrollbar>
</el-dialog>
</template>
<script setup lang="ts">
import { inject } from 'vue'
import { useI18n } from 'vue-i18n'
import type BaTable from '/@/utils/baTable'
import { timeFormat } from '/@/utils/common'
const baTable = inject('baTable') as BaTable
const { t } = useI18n()
</script>
<style scoped lang="scss">
.table-el-tree {
:deep(.el-tree-node) {
white-space: unset;
}
:deep(.el-tree-node__content) {
display: block;
align-items: unset;
height: unset;
}
}
</style>

View File

@@ -0,0 +1,179 @@
<template>
<div class="default-main ba-table-box">
<el-alert
class="ba-table-alert group-super-alert"
v-if="!adminInfo.super"
:title="t('auth.group.Manage subordinate role groups here')"
type="info"
show-icon
/>
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<!-- 表格顶部菜单 -->
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'unfold', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('auth.group.GroupName') })"
/>
<!-- 表格 -->
<!-- 要使用`el-table`组件原有的属性直接加在Table标签上即可 -->
<Table ref="tableRef" :pagination="false" />
<!-- 表单 -->
<PopupForm ref="formRef" />
</div>
</template>
<script setup lang="ts">
import { onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { getAdminRules } from '/@/api/backend/auth/group'
import { baTableApi } from '/@/api/common'
import { defaultOptButtons } from '/@/components/table'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import { useAdminInfo } from '/@/stores/adminInfo'
import baTableClass from '/@/utils/baTable'
import { getArrayKey } from '/@/utils/common'
import { uuid } from '/@/utils/random'
defineOptions({
name: 'auth/group',
})
const { t } = useI18n()
const adminInfo = useAdminInfo()
const formRef = useTemplateRef('formRef')
const tableRef = useTemplateRef('tableRef')
const baTable: baTableClass = new baTableClass(
new baTableApi('/admin/auth.Group/'),
{
expandAll: true,
dblClickNotEditColumn: [undefined],
column: [
{ type: 'selection', align: 'center' },
{ label: t('auth.group.Group name'), prop: 'name', align: 'left', width: '200' },
{ label: t('auth.group.jurisdiction'), prop: 'rules', align: 'center' },
{
label: t('State'),
prop: 'status',
align: 'center',
render: 'tag',
custom: { 0: 'danger', 1: 'success' },
replaceValue: { 0: t('Disable'), 1: t('Enable') },
},
{ label: t('Update time'), prop: 'update_time', align: 'center', width: '160', render: 'datetime' },
{ label: t('Create time'), prop: 'create_time', align: 'center', width: '160', render: 'datetime' },
{ label: t('Operate'), align: 'center', width: '130', render: 'buttons', buttons: defaultOptButtons(['edit', 'delete']) },
],
},
{
defaultItems: {
status: 1,
},
}
)
// 利用提交前钩子重写提交操作
baTable.before.onSubmit = ({ formEl, operate, items }) => {
let submitCallback = () => {
baTable.form.submitLoading = true
baTable.api
.postData(operate, {
...items,
rules: formRef.value?.getCheckeds(),
})
.then((res) => {
baTable.onTableHeaderAction('refresh', {})
baTable.form.submitLoading = false
baTable.form.operateIds?.shift()
if (baTable.form.operateIds!.length > 0) {
baTable.toggleForm('Edit', baTable.form.operateIds)
} else {
baTable.toggleForm()
}
baTable.runAfter('onSubmit', { res })
})
.catch(() => {
baTable.form.submitLoading = false
})
}
if (formEl) {
baTable.form.ref = formEl
formEl.validate((valid) => {
if (valid) {
submitCallback()
}
})
} else {
submitCallback()
}
return false
}
// 利用双击单元格前钩子重写双击操作
baTable.before.onTableDblclick = ({ row }) => {
return baTable.table.extend!.adminGroup.indexOf(row.id) === -1
}
// 获取到数据后钩子
baTable.after.getData = ({ res }) => {
baTable.table.extend!.adminGroup = res.data.group
let buttonsKey = getArrayKey(baTable.table.column, 'render', 'buttons')
baTable.table.column[buttonsKey].buttons!.forEach((value: OptButton) => {
value.display = (row) => {
return res.data.group.indexOf(row.id) === -1
}
})
}
// 切换表单后钩子
baTable.after.toggleForm = ({ operate }) => {
if (operate == 'Add') {
menuRuleTreeUpdate()
}
}
// 编辑请求完成后钩子
baTable.after.getEditData = () => {
menuRuleTreeUpdate()
}
const menuRuleTreeUpdate = () => {
getAdminRules().then((res) => {
baTable.form.extend!.menuRules = res.data.list
if (baTable.form.items!.rules && baTable.form.items!.rules.length) {
if (baTable.form.items!.rules.includes('*')) {
let arr: number[] = []
for (const key in baTable.form.extend!.menuRules) {
arr.push(baTable.form.extend!.menuRules[key].id)
}
baTable.form.extend!.defaultCheckedKeys = arr
} else {
baTable.form.extend!.defaultCheckedKeys = baTable.form.items!.rules
}
} else {
baTable.form.extend!.defaultCheckedKeys = []
}
baTable.form.extend!.treeKey = uuid()
})
}
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()
})
</script>
<style scoped lang="scss">
.group-super-alert {
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<!-- 对话框表单 -->
<el-dialog
class="ba-operate-dialog"
:close-on-click-modal="false"
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
@close="baTable.toggleForm"
:destroy-on-close="true"
>
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
</div>
</template>
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
<div
class="ba-operate-form"
:class="'ba-' + baTable.form.operate + '-form'"
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
>
<el-form
ref="formRef"
@keyup.enter="baTable.onSubmit(formRef)"
:model="baTable.form.items"
:label-position="config.layout.shrink ? 'top' : 'right'"
:label-width="baTable.form.labelWidth + 'px'"
:rules="rules"
v-if="!baTable.form.loading"
>
<FormItem
:label="t('auth.group.Parent group')"
v-model="baTable.form.items!.pid"
type="remoteSelect"
prop="pid"
:input-attr="{
params: { isTree: true },
field: 'name',
remoteUrl: baTable.api.actionUrl.get('index'),
placeholder: t('Click select'),
emptyValues: ['', null, undefined, 0],
valueOnClear: 0,
}"
/>
<el-form-item prop="name" :label="t('auth.group.Group name')">
<el-input
v-model="baTable.form.items!.name"
type="string"
:placeholder="t('Please input field', { field: t('auth.group.Group name') })"
></el-input>
</el-form-item>
<el-form-item prop="auth" :label="t('auth.group.jurisdiction')">
<el-tree
ref="treeRef"
:key="baTable.form.extend!.treeKey"
:default-checked-keys="baTable.form.extend!.defaultCheckedKeys"
:default-expand-all="true"
show-checkbox
node-key="id"
:props="{ children: 'children', label: 'title', class: treeNodeClass }"
:data="baTable.form.extend!.menuRules"
class="w100"
/>
</el-form-item>
<FormItem
:label="t('State')"
v-model="baTable.form.items!.status"
type="radio"
:input-attr="{
border: true,
content: { 0: t('Disable'), 1: t('Enable') },
}"
/>
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
<el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, inject, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import type baTableClass from '/@/utils/baTable'
import FormItem from '/@/components/formItem/index.vue'
import type { ElTree, FormItemRule } from 'element-plus'
import { buildValidatorData } from '/@/utils/validate'
import type Node from 'element-plus/es/components/tree/src/model/node'
import { useConfig } from '/@/stores/config'
const config = useConfig()
const formRef = useTemplateRef('formRef')
const treeRef = useTemplateRef('treeRef')
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
name: [buildValidatorData({ name: 'required', title: t('auth.group.Group name') })],
auth: [
{
required: true,
validator: (rule: any, val: string, callback: Function) => {
let ids = getCheckeds()
if (ids.length <= 0) {
return callback(new Error(t('Please select field', { field: t('auth.group.jurisdiction') })))
}
return callback()
},
},
],
pid: [
{
validator: (rule: any, val: string, callback: Function) => {
if (!val) {
return callback()
}
if (parseInt(val) == parseInt(baTable.form.items!.id)) {
return callback(new Error(t('auth.group.The parent group cannot be the group itself')))
}
return callback()
},
trigger: 'blur',
},
],
})
const getCheckeds = () => {
return treeRef.value!.getCheckedKeys().concat(treeRef.value!.getHalfCheckedKeys())
}
const treeNodeClass = (data: anyObj, node: Node) => {
if (node.isLeaf) return ''
let addClass = true
for (const key in node.childNodes) {
if (!node.childNodes[key].isLeaf) {
addClass = false
}
}
return addClass ? 'penultimate-node' : ''
}
defineExpose({
getCheckeds,
})
</script>
<style scoped lang="scss">
:deep(.penultimate-node) {
.el-tree-node__children {
padding-left: 60px;
white-space: pre-wrap;
line-height: 12px;
.el-tree-node {
display: inline-block;
}
.el-tree-node__content {
padding-left: 5px !important;
padding-right: 5px;
.el-tree-node__expand-icon {
display: none;
}
}
}
}
</style>

View File

@@ -0,0 +1,196 @@
<template>
<div class="default-main ba-table-box">
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
<!-- 表格顶部菜单 -->
<TableHeader
:buttons="['refresh', 'add', 'edit', 'delete', 'unfold', 'quickSearch', 'columnDisplay']"
:quick-search-placeholder="t('Quick search placeholder', { fields: t('auth.rule.title') })"
/>
<!-- 设置合适的 max-height 实现隐藏布局主体部分本身的滚动条这样就可以监听表格的 @scroll -->
<!-- max-height = 100vh - (当前布局顶栏高度 + 表头栏高度 + 表格上边距 + 预留的底部下边距) -->
<Table
ref="tableRef"
:max-height="`calc(-${adminLayoutHeaderBarHeight[config.layout.layoutMode as keyof typeof adminLayoutHeaderBarHeight] + 75 + 16}px + 100vh)`"
:pagination="false"
@expand-change="onExpandChange"
@scroll="onScroll"
/>
<!-- 表单 -->
<PopupForm />
</div>
</template>
<script setup lang="ts">
import { cloneDeep, debounce } from 'lodash-es'
import { nextTick, onMounted, provide, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import PopupForm from './popupForm.vue'
import { baTableApi } from '/@/api/common'
import { defaultOptButtons } from '/@/components/table'
import TableHeader from '/@/components/table/header/index.vue'
import Table from '/@/components/table/index.vue'
import { useConfig } from '/@/stores/config'
import baTableClass from '/@/utils/baTable'
import { adminLayoutHeaderBarHeight } from '/@/utils/layout'
defineOptions({
name: 'auth/rule',
})
const { t } = useI18n()
const config = useConfig()
const tableRef = useTemplateRef('tableRef')
const baTable = new baTableClass(
new baTableApi('/admin/auth.Rule/'),
{
expandAll: false,
dblClickNotEditColumn: [undefined, 'keepalive', 'status'],
column: [
{ type: 'selection', align: 'center' },
{ label: t('auth.rule.title'), prop: 'title', align: 'left', width: '200' },
{ label: t('auth.rule.Icon'), prop: 'icon', align: 'center', width: '60', render: 'icon', default: 'fa fa-circle-o' },
{ label: t('auth.rule.name'), prop: 'name', align: 'center', showOverflowTooltip: true },
{
label: t('auth.rule.type'),
prop: 'type',
align: 'center',
render: 'tag',
custom: { menu: 'danger', menu_dir: 'success', button: 'info' },
replaceValue: { menu: t('auth.rule.type menu'), menu_dir: t('auth.rule.type menu_dir'), button: t('auth.rule.type button') },
},
{ label: t('auth.rule.cache'), prop: 'keepalive', align: 'center', width: '80', render: 'switch' },
{ label: t('State'), prop: 'status', align: 'center', width: '80', render: 'switch' },
{ label: t('Update time'), prop: 'update_time', align: 'center', width: '160', render: 'datetime' },
{
label: t('Operate'),
align: 'center',
width: '130',
render: 'buttons',
buttons: defaultOptButtons(),
},
],
dragSortLimitField: 'pid',
},
{
defaultItems: {
type: 'menu',
menu_type: 'tab',
extend: 'none',
keepalive: 0,
status: 1,
icon: 'fa fa-circle-o',
buttons: ['index', 'add', 'edit', 'del'],
},
}
)
/**
* 内存缓存表格的一些状态数据,供数据刷新后恢复
*/
const sessionStateDefault = {
expanded: [] as any[],
scrollTop: 0,
scrollLeft: 0,
expandAll: false,
}
let sessionState = sessionStateDefault
/**
* 记录表格行展开状态
*/
const onExpandChange = (row: any, expanded: boolean) => {
if (expanded) {
sessionState.expanded.push(row)
} else {
sessionState.expanded = sessionState.expanded.filter((item: any) => item.id !== row.id)
}
}
/**
* 记录表格滚动条位置
*/
const onScroll = debounce(({ scrollLeft, scrollTop }: { scrollLeft: number; scrollTop: number }) => {
sessionState.scrollTop = scrollTop
sessionState.scrollLeft = scrollLeft
}, 500)
/**
* 记录表格行展开折叠状态
*/
const onUnfoldAll = (state: boolean) => {
sessionState.expandAll = state
}
/**
* 恢复已记录的表格状态
*/
const restoreState = () => {
nextTick(() => {
const sessionStateTemp = sessionState
// 重置 sessionState 为默认值,恢复缓存的记录时将自动重设
sessionState = cloneDeep(sessionStateDefault)
for (const key in sessionStateTemp.expanded) {
tableRef.value?.getRef()?.toggleRowExpansion(sessionStateTemp.expanded[key], true)
}
nextTick(() => {
if (sessionStateTemp.scrollTop || sessionStateTemp.scrollLeft) {
tableRef.value?.getRef()?.scrollTo({ top: sessionStateTemp.scrollTop || 0, left: sessionStateTemp.scrollLeft || 0 })
}
/**
* expandAll 为 “是否默认展开所有行”
* 此处表格数据已渲染,仅做顶部按钮状态标记用,不会实际上的执行展开折叠操作
* 展开全部行之后再只对某一行进行折叠时expandAll 不会改变,所以此处并不根据 expandAll 值执行折叠展开所有行的操作
*/
baTable.table.expandAll = sessionStateTemp.expandAll
onUnfoldAll(sessionStateTemp.expandAll)
})
})
}
// 获取数据前钩子
baTable.before.getData = () => {
baTable.table.expandAll = baTable.table.filter?.quickSearch ? true : false
}
// 获取到编辑行数据后的钩子
baTable.after.getEditData = () => {
if (baTable.form.items && !baTable.form.items.icon) {
baTable.form.items.icon = 'fa fa-circle-o'
}
}
// 表格顶部按钮事件触发后的钩子
baTable.after.onTableHeaderAction = ({ event, data }) => {
if (event == 'unfold') {
onUnfoldAll(data.unfold)
}
}
// 获取到表格数据后的钩子
baTable.after.getData = () => {
restoreState()
}
provide('baTable', baTable)
onMounted(() => {
baTable.table.ref = tableRef.value
baTable.mount()
baTable.getData()?.then(() => {
baTable.dragSort()
})
})
</script>
<style scoped lang="scss">
.default-main {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,244 @@
<template>
<!-- 对话框表单 -->
<el-dialog
class="ba-operate-dialog"
:close-on-click-modal="false"
:destroy-on-close="true"
:model-value="['Add', 'Edit'].includes(baTable.form.operate!)"
@close="baTable.toggleForm"
>
<template #header>
<div class="title" v-drag="['.ba-operate-dialog', '.el-dialog__header']" v-zoom="'.ba-operate-dialog'">
{{ baTable.form.operate ? t(baTable.form.operate) : '' }}
</div>
</template>
<el-scrollbar v-loading="baTable.form.loading" class="ba-table-form-scrollbar">
<div
class="ba-operate-form"
:class="'ba-' + baTable.form.operate + '-form'"
:style="config.layout.shrink ? '' : 'width: calc(100% - ' + baTable.form.labelWidth! / 2 + 'px)'"
>
<el-form
ref="formRef"
@keyup.enter="baTable.onSubmit(formRef)"
:model="baTable.form.items"
:label-position="config.layout.shrink ? 'top' : 'right'"
:label-width="baTable.form.labelWidth + 'px'"
:rules="rules"
v-if="!baTable.form.loading"
>
<FormItem
type="remoteSelect"
prop="pid"
:label="t('auth.rule.Superior menu rule')"
v-model="baTable.form.items!.pid"
:placeholder="t('Click select')"
:input-attr="{
params: { isTree: true },
field: 'title',
remoteUrl: baTable.api.actionUrl.get('index'),
emptyValues: ['', null, undefined, 0],
valueOnClear: 0,
}"
/>
<FormItem
:label="t('auth.rule.Rule type')"
v-model="baTable.form.items!.type"
type="radio"
:input-attr="{
border: true,
content: { menu_dir: t('auth.rule.type menu_dir'), menu: t('auth.rule.type menu'), button: t('auth.rule.type button') },
}"
/>
<el-form-item prop="title" :label="t('auth.rule.Rule title')">
<el-input
v-model="baTable.form.items!.title"
type="string"
:placeholder="t('Please input field', { field: t('auth.rule.Rule title') })"
></el-input>
</el-form-item>
<el-form-item prop="name" :label="t('auth.rule.Rule name')">
<el-input
v-model="baTable.form.items!.name"
type="string"
:placeholder="t('auth.rule.English name, which does not need to start with `/admin`, such as auth/menu')"
></el-input>
<div class="block-help">
{{ t('auth.rule.It will be registered as the web side routing name and used as the server side API authentication') }}
</div>
</el-form-item>
<el-form-item prop="path" v-if="baTable.form.items!.type != 'button'" :label="t('auth.rule.Routing path')">
<el-input
v-model="baTable.form.items!.path"
type="string"
:placeholder="t('auth.rule.The web side routing path (path) does not need to start with `/admin`, such as auth/menu')"
></el-input>
</el-form-item>
<FormItem
v-if="baTable.form.operate && baTable.form.items!.type != 'button'"
type="icon"
:label="t('auth.rule.Rule Icon')"
v-model="baTable.form.items!.icon"
:input-attr="{
showIconName: true,
}"
/>
<FormItem
v-if="baTable.form.items!.type == 'menu'"
:label="t('auth.rule.Menu type')"
v-model="baTable.form.items!.menu_type"
type="radio"
:input-attr="{
border: true,
content: { tab: t('auth.rule.Menu type tab'), link: t('auth.rule.Menu type link (offsite)'), iframe: 'Iframe' },
}"
/>
<el-form-item
prop="url"
v-if="baTable.form.items!.menu_type != 'tab' && baTable.form.items!.type == 'menu'"
:label="t('auth.rule.Link address')"
>
<el-input
v-model="baTable.form.items!.url"
type="string"
:placeholder="t('auth.rule.Please enter the URL address of the link or iframe')"
></el-input>
</el-form-item>
<el-form-item
prop="component"
v-if="baTable.form.items!.type == 'menu' && baTable.form.items!.menu_type == 'tab'"
:label="t('auth.rule.Component path')"
>
<el-input
v-model="baTable.form.items!.component"
type="string"
:placeholder="t('auth.rule.Web side component path, please start with /src, such as: /src/views/backend/dashboard')"
></el-input>
</el-form-item>
<el-form-item
v-if="baTable.form.items!.type == 'menu' && baTable.form.items!.menu_type == 'tab'"
:label="t('auth.rule.Extended properties')"
>
<el-select
class="w100"
v-model="baTable.form.items!.extend"
:placeholder="t('Please select field', { field: t('auth.rule.Extended properties') })"
>
<el-option :label="t('auth.rule.none')" value="none"></el-option>
<el-option :label="t('auth.rule.Add as route only')" value="add_rules_only"></el-option>
<el-option :label="t('auth.rule.Add as menu only')" value="add_menu_only"></el-option>
</el-select>
<div class="block-help">{{ t('auth.rule.extend Title') }}</div>
</el-form-item>
<el-form-item :label="t('auth.rule.Rule comments')">
<el-input
@keyup.enter.stop=""
@keyup.ctrl.enter="baTable.onSubmit(formRef)"
v-model="baTable.form.items!.remark"
type="textarea"
:autosize="{ minRows: 2, maxRows: 5 }"
:placeholder="
t(
'auth.rule.Use in controller `get_ route_ Remark()` function, which can obtain the value of this field for your own use, such as the banner file of the console'
)
"
></el-input>
</el-form-item>
<el-form-item :label="t('auth.rule.Rule weight')">
<el-input
v-model="baTable.form.items!.weigh"
type="number"
:placeholder="t('auth.rule.Please enter the weight of menu rule (sort by)')"
></el-input>
</el-form-item>
<FormItem
v-if="baTable.form.operate == 'Add' && baTable.form.items!.type == 'menu'"
:label="t('auth.rule.Create Page Button')"
v-model="baTable.form.items!.buttons"
type="selects"
:input-attr="{
content: {
index: t('auth.rule.Create Page Button index'),
add: t('auth.rule.Create Page Button add'),
edit: t('auth.rule.Create Page Button edit'),
del: t('auth.rule.Create Page Button del'),
sortable: t('auth.rule.Create Page Button sortable'),
},
}"
:placeholder="t('auth.rule.Please select the button for automatically creating the desired page')"
:block-help="t('auth.rule.Create Page Button tips')"
/>
<FormItem
:label="t('auth.rule.cache')"
v-model="baTable.form.items!.keepalive"
type="radio"
:input-attr="{
border: true,
content: { 0: t('Disable'), 1: t('Enable') },
}"
/>
<FormItem
:label="t('State')"
v-model="baTable.form.items!.status"
type="radio"
:input-attr="{
border: true,
content: { 0: t('Disable'), 1: t('Enable') },
}"
/>
</el-form>
</div>
</el-scrollbar>
<template #footer>
<div :style="'width: calc(100% - ' + baTable.form.labelWidth! / 1.8 + 'px)'">
<el-button @click="baTable.toggleForm('')">{{ t('Cancel') }}</el-button>
<el-button v-blur :loading="baTable.form.submitLoading" @click="baTable.onSubmit(formRef)" type="primary">
{{ baTable.form.operateIds && baTable.form.operateIds.length > 1 ? t('Save and edit next item') : t('Save') }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, inject, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import type baTableClass from '/@/utils/baTable'
import FormItem from '/@/components/formItem/index.vue'
import { buildValidatorData } from '/@/utils/validate'
import type { FormItemRule } from 'element-plus'
import { useConfig } from '/@/stores/config'
const config = useConfig()
const formRef = useTemplateRef('formRef')
const baTable = inject('baTable') as baTableClass
const { t } = useI18n()
const rules: Partial<Record<string, FormItemRule[]>> = reactive({
title: [buildValidatorData({ name: 'required', title: t('auth.rule.Rule title') })],
name: [buildValidatorData({ name: 'required', title: t('auth.rule.Rule name') })],
path: [buildValidatorData({ name: 'required', title: t('auth.rule.Routing path') })],
url: [
buildValidatorData({ name: 'required', title: t('auth.rule.Link address') }),
buildValidatorData({ name: 'url', message: t('auth.rule.Please enter the correct URL') }),
],
component: [buildValidatorData({ name: 'required', message: t('auth.rule.Component path') })],
pid: [
{
validator: (rule: any, val: string, callback: Function) => {
if (!val) {
return callback()
}
if (parseInt(val) == parseInt(baTable.form.items!.id)) {
return callback(new Error(t('auth.rule.The superior menu rule cannot be the rule itself')))
}
return callback()
},
trigger: 'blur',
},
],
})
</script>
<style scoped lang="scss"></style>