first commit

This commit is contained in:
2025-12-26 23:19:09 +08:00
commit b29d128e41
788 changed files with 100922 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
export default {
audit: {
index: '#',
importsysAuditLogTip: 'import SysAuditLog',
id: 'id',
auditName: 'auditName',
auditField: 'auditField',
beforeVal: 'beforeVal',
afterVal: 'afterVal',
createBy: 'createBy',
createTime: 'createTime',
delFlag: 'delFlagx',
tenantId: 'tenantId',
inputIdTip: 'input id',
inputAuditNameTip: 'input auditName',
inputAuditFieldTip: 'input auditField',
inputBeforeValTip: 'input beforeVal',
inputAfterValTip: 'input afterVal',
inputCreateByTip: 'input createBy',
inputCreateTimeTip: 'input createTime',
inputDelFlagTip: 'input delFlagx',
inputTenantIdTip: 'input tenantId',
},
};

View File

@@ -0,0 +1,24 @@
export default {
audit: {
index: '#',
importsysAuditLogTip: '导入审计记录表',
id: '主键',
auditName: '审计名称',
auditField: '字段名称',
beforeVal: '变更前值',
afterVal: '变更后值',
createBy: '操作人',
createTime: '操作时间',
delFlag: '删除标记',
tenantId: '租户ID',
inputIdTip: '请输入主键',
inputAuditNameTip: '请输入审计名称',
inputAuditFieldTip: '请输入字段名称',
inputBeforeValTip: '请输入变更前值',
inputAfterValTip: '请输入变更后值',
inputCreateByTip: '请输入操作人',
inputCreateTimeTip: '请输入操作时间',
inputDelFlagTip: '请输入删除标记',
inputTenantIdTip: '请输入租户ID',
},
};

View File

@@ -0,0 +1,137 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item :label="$t('audit.auditName')" prop="auditName">
<el-input :placeholder="t('audit.inputAuditNameTip')" v-model="state.queryForm.auditName" style="max-width: 180px" />
</el-form-item>
<el-form-item :label="$t('audit.auditField')" prop="auditField">
<el-input :placeholder="t('audit.inputAuditFieldTip')" v-model="state.queryForm.auditField" style="max-width: 180px" />
</el-form-item>
<el-form-item :label="$t('audit.createBy')" prop="createBy">
<el-input :placeholder="t('audit.inputCreateByTip')" v-model="state.queryForm.createBy" style="max-width: 180px" />
</el-form-item>
<el-form-item>
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button icon="Refresh" formDialogRef @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button
plain
formDialogRef
:disabled="multiple"
icon="Delete"
type="primary"
class="ml10"
v-auth="'sys_audit_del'"
@click="handleDelete(selectObjs)"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_audit_export'"
@exportExcel="exportExcel"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
style="width: 100%"
row-key="id"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" :label="$t('audit.index')" width="80" />
<el-table-column prop="auditName" :label="$t('audit.auditName')" show-overflow-tooltip />
<el-table-column prop="auditField" :label="$t('audit.auditField')" show-overflow-tooltip />
<el-table-column prop="beforeVal" :label="$t('audit.beforeVal')" show-overflow-tooltip />
<el-table-column prop="afterVal" :label="$t('audit.afterVal')" show-overflow-tooltip />
<el-table-column prop="createBy" :label="$t('audit.createBy')" show-overflow-tooltip />
<el-table-column prop="createTime" :label="$t('audit.createTime')" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="delete" text type="primary" v-auth="'sys_audit_del'" @click="handleDelete([scope.row.id])"
>{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</div>
</template>
<script setup lang="ts" name="systemSysAuditLog">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObjs } from '/@/api/admin/audit';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
// table hook
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
descs: ['create_time'],
});
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/audit/export', Object.assign(state.queryForm,{ids:selectObjs}), 'audit.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObjs(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,245 @@
<template>
<el-dialog :close-on-click-modal="false" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" draggable v-model="visible">
<el-form :model="form" :rules="dataRules" formDialogRef label-width="120px" ref="dataFormRef" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="t('client.clientId')" prop="clientId">
<el-input :placeholder="t('client.inputClientIdTip')" v-model="form.clientId" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('client.clientSecret')" prop="clientSecret">
<el-input :placeholder="t('client.inputClientSecretTip')" v-model="form.clientSecret" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('client.scope')" prop="scope">
<el-input :placeholder="t('client.inputScopeTip')" v-model="form.scope" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('client.authorizedGrantTypes')" prop="authorizedGrantTypes">
<el-select collapse-tags collapse-tags-tooltip multiple v-model="form.authorizedGrantTypes">
<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in grant_types"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('client.accessTokenValidity')" prop="accessTokenValidity">
<el-input-number :placeholder="t('client.inputAccessTokenValidityTip')" v-model="form.accessTokenValidity" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('client.refreshTokenValidity')" prop="refreshTokenValidity">
<el-input-number :placeholder="t('client.inputRefreshTokenValidityTip')" v-model="form.refreshTokenValidity" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.authorizedGrantTypes.includes('authorization_code')">
<el-form-item :label="t('client.autoapprove')" prop="autoapprove">
<el-radio-group v-model="form.autoapprove">
<el-radio :key="index" :label="item.value" border v-for="(item, index) in common_status">{{ item.label }} </el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20" v-if="form.authorizedGrantTypes.includes('authorization_code')">
<el-form-item :label="t('client.authorities')" prop="authorities">
<el-input :placeholder="t('client.inputAuthoritiesTip')" v-model="form.authorities" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20" v-if="form.authorizedGrantTypes.includes('authorization_code')">
<el-form-item :label="t('client.webServerRedirectUri')" prop="webServerRedirectUri">
<el-input :placeholder="t('client.inputWebServerRedirectUriTip')" v-model="form.webServerRedirectUri" />
</el-form-item>
</el-col>
</el-row>
<el-collapse v-model="collapseActive">
<el-collapse-item name="1" title="安全属性">
<template #title>
<el-icon class="header-icon">
<info-filled />
</el-icon>
安全属性
</template>
<el-row>
<el-col :span="12" class="mb20">
<el-form-item :label="t('client.captchaFlag')" prop="captchaFlag">
<el-radio-group v-model="form.captchaFlag">
<el-radio :key="index" :label="item.value" border v-for="(item, index) in captcha_flag_types">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('client.encFlag')" prop="encFlag">
<el-radio-group v-model="form.encFlag">
<el-radio :key="index" :label="item.value" border v-for="(item, index) in enc_flag_types">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('client.onlineQuantity')" prop="onlineQuantity">
<el-radio-group v-model="form.onlineQuantity">
<el-radio :key="index" :label="item.value" border v-for="(item, index) in enc_flag_types">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-collapse-item>
</el-collapse>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="SysOauthClientDetailsDialog" setup>
import { useDict } from '/@/hooks/dict';
import { useMessage } from '/@/hooks/message';
import { addObj, getObj, putObj, validateclientId } from '/@/api/admin/client';
import { useI18n } from 'vue-i18n';
import { rule } from '/@/utils/validate';
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 定义字典
const { grant_types, common_status, captcha_flag_types, enc_flag_types } = useDict(
'grant_types',
'common_status',
'captcha_flag_types',
'enc_flag_types'
);
// 提交表单数据
const form = reactive({
id: '',
clientId: '',
clientSecret: '',
scope: 'server',
authorizedGrantTypes: [] as string[],
webServerRedirectUri: '',
authorities: '',
accessTokenValidity: 43200,
refreshTokenValidity: 2592001,
autoapprove: 'true',
delFlag: '',
createBy: '',
updateBy: '',
createTime: '',
updateTime: '',
tenantId: '',
onlineQuantity: '1',
captchaFlag: '1',
encFlag: '1',
});
const collapseActive = ref('1');
// 定义校验规则
const dataRules = ref({
clientId: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '编号不能为空', trigger: 'blur' },
{ validator: rule.validatorLowercase, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateclientId(rule, value, callback, form.id !== '');
},
trigger: 'blur',
},
],
clientSecret: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '密钥不能为空', trigger: 'blur' },
{ validator: rule.validatorLower, trigger: 'blur' },
],
scope: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '域不能为空', trigger: 'blur' }],
authorizedGrantTypes: [{ required: true, message: '授权模式不能为空', trigger: 'blur' }],
accessTokenValidity: [
{ required: true, message: '令牌时效不能为空', trigger: 'blur' },
{ type: 'number', min: 1, message: '令牌时效不能小于一小时', trigger: 'blur' },
],
refreshTokenValidity: [
{ required: true, message: '刷新时效不能为空', trigger: 'blur' },
{ type: 'number', min: 1, message: '刷新时效不能小于两小时', trigger: 'blur' },
],
captchaFlag: [{ required: true, message: '是否开启验证码校验', trigger: 'blur' }],
encFlag: [{ required: true, message: '是否开启密码加密传输', trigger: 'blur' }],
onlineQuantity: [{ required: true, message: '是否允许同时在线', trigger: 'blur' }],
autoapprove: [{ required: true, message: '自动放行不能为空', trigger: 'blur' }],
webServerRedirectUri: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '回调地址不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.id = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取sysOauthClientDetails信息
if (id) {
form.id = id;
getsysOauthClientDetailsData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
useMessage().success(t(form.id ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getsysOauthClientDetailsData = (id: string) => {
// 获取数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,45 @@
export default {
client: {
index: '#',
importsysOauthClientDetailsTip: 'import SysOauthClientDetails',
id: 'id',
clientId: 'client Id',
resourceIds: 'resourceIds',
clientSecret: 'clientSecret',
scope: 'scope',
authorizedGrantTypes: 'authorizedGrantTypes',
webServerRedirectUri: 'webServerRedirectUri',
authorities: 'authorities',
accessTokenValidity: 'accessTokenValidity',
refreshTokenValidity: 'refreshTokenValidity',
additionalInformation: 'additionalInformation',
autoapprove: 'autoapprove',
delFlag: 'delFlag',
createBy: 'createBy',
updateBy: 'updateBy',
createTime: 'createTime',
updateTime: 'updateTime',
tenantId: 'tenantId',
captchaFlag: 'captchaFlag',
encFlag: 'encFlag',
onlineQuantity: 'onlineQuantity',
inputIdTip: 'input id',
inputClientIdTip: 'input clientId',
inputResourceIdsTip: 'input resourceIds',
inputClientSecretTip: 'input clientSecret',
inputScopeTip: 'input scope',
inputAuthorizedGrantTypesTip: 'input authorizedGrantTypes',
inputWebServerRedirectUriTip: 'input webServerRedirectUri',
inputAuthoritiesTip: 'input authorities',
inputAccessTokenValidityTip: 'input accessTokenValidity',
inputRefreshTokenValidityTip: 'input refreshTokenValidity',
inputAdditionalInformationTip: 'input additionalInformation',
inputAutoapproveTip: 'input autoapprove',
inputDelFlagTip: 'input delFlag',
inputCreateByTip: 'input createBy',
inputUpdateByTip: 'input updateBy',
inputCreateTimeTip: 'input createTime',
inputUpdateTimeTip: 'input updateTime',
inputTenantIdTip: 'input tenantId',
},
};

View File

@@ -0,0 +1,43 @@
export default {
client: {
index: '#',
importsysOauthClientDetailsTip: '导入终端信息表',
id: 'ID',
clientId: '客户端id',
resourceIds: '',
clientSecret: '密钥',
scope: '域',
authorizedGrantTypes: '授权模式',
webServerRedirectUri: '回调地址',
authorities: '权限',
accessTokenValidity: '令牌时效(秒)',
refreshTokenValidity: '刷新时效(秒)',
additionalInformation: '扩展信息',
autoapprove: '自动放行',
createBy: '创建人',
updateBy: '修改人',
createTime: '上传时间',
updateTime: '更新时间',
tenantId: '所属租户',
captchaFlag: '验证码开关',
encFlag: '前端密码加密',
onlineQuantity: '允许同时在线',
inputIdTip: '请输入ID',
inputClientIdTip: '请输入客户端id',
inputResourceIdsTip: '请输入',
inputClientSecretTip: '请输入密钥',
inputScopeTip: '请输入域',
inputAuthorizedGrantTypesTip: '请输入授权模式',
inputWebServerRedirectUriTip: '请输入回调地址',
inputAuthoritiesTip: '请输入权限',
inputAccessTokenValidityTip: '请输入令牌时效',
inputRefreshTokenValidityTip: '请输入刷新时效',
inputAdditionalInformationTip: '请输入扩展信息',
inputAutoapproveTip: '请输入自动放行',
inputCreateByTip: '请输入创建人',
inputUpdateByTip: '请输入修改人',
inputCreateTimeTip: '请输入上传时间',
inputUpdateTimeTip: '请输入更新时间',
inputTenantIdTip: '请输入所属租户',
},
};

View File

@@ -0,0 +1,155 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('client.clientId')" prop="clientId">
<el-input :placeholder="$t('client.clientId')" style="max-width: 180px" v-model="state.queryForm.clientId" />
</el-form-item>
<el-form-item :label="$t('client.clientSecret')" prop="clientSecret">
<el-input :placeholder="$t('client.clientSecret')" style="max-width: 180px" v-model="state.queryForm.clientSecret" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'sys_client_add'" @click="formDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary">
{{ $t('common.addBtn') }}
</el-button>
<el-button v-auth="'sys_client_del'" plain @click="handleRefreshCache()" class="ml10" icon="refresh-left" type="primary">
{{ $t('common.refreshCacheBtn') }}
</el-button>
<el-button plain :disabled="multiple" @click="handleDelete(selectObjs)" class="ml10" icon="Delete" type="primary" v-auth="'sys_client_del'">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'sys_client_del'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
style="width: 100%"
row-key="id"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="t('client.index')" type="index" width="60" />
<el-table-column :label="t('client.clientId')" prop="clientId" show-overflow-tooltip />
<el-table-column :label="t('client.clientSecret')" prop="clientSecret" show-overflow-tooltip />
<el-table-column :label="t('client.scope')" prop="scope" show-overflow-tooltip />
<el-table-column :label="t('client.authorizedGrantTypes')" prop="authorizedGrantTypes" show-overflow-tooltip width="400px">
<template #default="scope">
<dict-tag :options="grant_types" :value="scope.row.authorizedGrantTypes" />
</template>
</el-table-column>
<el-table-column :label="t('client.accessTokenValidity')" prop="accessTokenValidity" show-overflow-tooltip />
<el-table-column :label="t('client.refreshTokenValidity')" prop="refreshTokenValidity" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" @click="formDialogRef.openDialog(scope.row.clientId)" text type="primary" v-auth="'sys_client_add'"
>{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" @click="handleDelete([scope.row.id])" text type="primary" v-auth="'sys_client_del'">
{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog @refresh="getDataList()" ref="formDialogRef" />
</div>
</template>
<script lang="ts" name="systemSysOauthClientDetails" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchList, refreshCache } from '/@/api/admin/client';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
import { useI18n } from 'vue-i18n';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义查询字典
const { grant_types } = useDict('grant_types');
// 定义变量内容
const formDialogRef = ref();
const queryRef = ref();
// 搜索变量
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
descs: ['id'],
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 删除缓存
const handleRefreshCache = () => {
refreshCache().then(() => {
useMessage().success('同步成功');
});
};
const resetQuery = () => {
queryRef.value.resetFields();
// state.queryForm = {};
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/client/export', Object.assign(state.queryForm,{ids:selectObjs}), 'client.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,133 @@
<template>
<el-dialog v-model="visible" :title="dataForm.id ? $t('common.editBtn') : $t('common.addBtn')" width="600">
<el-form ref="dicDialogFormRef" :model="dataForm" label-width="90px" :rules="dataRules" v-loading="loading">
<el-form-item :label="$t('dictItem.dictType')" prop="dictType">
<el-input v-model="dataForm.dictType" clearable disabled
:placeholder="$t('dictItem.inputDictTypeTip')"></el-input>
</el-form-item>
<el-form-item :label="$t('dictItem.label')" prop="label">
<el-input v-model="dataForm.label" :placeholder="$t('dictItem.inputLabelTip')" clearable></el-input>
</el-form-item>
<el-form-item :label="$t('dictItem.itemValue')" prop="value">
<el-input v-model="dataForm.value" :placeholder="$t('dictItem.inputItemValueTip')" clearable></el-input>
</el-form-item>
<el-form-item :label="$t('dictItem.description')" prop="description">
<el-input v-model="dataForm.description" :placeholder="$t('dictItem.inputDescriptionTip')" clearable></el-input>
</el-form-item>
<el-form-item :label="$t('dictItem.sortOrder')" prop="sortOrder">
<el-input-number v-model="dataForm.sortOrder" :placeholder="$t('dictItem.inputSortOrderTip')"
clearable></el-input-number>
</el-form-item>
<el-form-item :label="$t('dictItem.remarks')" prop="remarks">
<el-input type="textarea" maxlength="100" :rows="3" v-model="dataForm.remarks"
:placeholder="$t('dictItem.inputRemarksTip')"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="dict-item-form">
import {useI18n} from 'vue-i18n';
import {getItemObj, addItemObj, putItemObj, validateDictItemLabel} from '/@/api/admin/dict';
import {useMessage} from '/@/hooks/message';
import {rule} from "/@/utils/validate";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const {t} = useI18n();
// 定义变量内容
const dicDialogFormRef = ref();
const visible = ref(false);
const loading = ref(false);
const dataForm = reactive({
id: '',
dictId: '',
dictType: '',
value: '',
label: '',
description: '',
sortOrder: 0,
remarks: '',
});
const dataRules = reactive({
dictType: [{validator: rule.overLength, trigger: 'blur'}, {
required: true,
message: '请点选左侧字典项',
trigger: 'blur'
}],
value: [{validator: rule.overLength, trigger: 'blur'}, {required: true, message: '数据值不能为空', trigger: 'blur'}],
label: [
{validator: rule.overLength, trigger: 'blur'},
{required: true, message: '标签不能为空', trigger: 'blur'},
{
validator: (rule: any, value: any, callback: any) => {
validateDictItemLabel(rule, value, callback, dataForm.dictType, dataForm.id !== '');
},
trigger: 'blur',
},
],
description: [{validator: rule.overLength, trigger: 'blur'}, {
required: true,
message: '描述不能为空',
trigger: 'blur'
}],
sortOrder: [{validator: rule.overLength, trigger: 'blur'}, {
required: true,
message: '排序不能为空',
trigger: 'blur'
}],
});
// 打开弹窗
const openDialog = (row: any, dictForm: any) => {
visible.value = true;
dataForm.id = '';
nextTick(() => {
dicDialogFormRef.value?.resetFields();
if (dictForm) {
dataForm.dictId = dictForm.dictId;
dataForm.dictType = dictForm.dictType;
}
});
if (row?.id) {
getItemObj(row.id).then((res) => {
Object.assign(dataForm, res.data);
});
}
};
// 提交
const onSubmit = async () => {
const valid = await dicDialogFormRef.value.validate().catch(() => {
});
if (!valid) return false;
try {
loading.value = true;
dataForm.id ? await putItemObj(dataForm) : await addItemObj(dataForm);
useMessage().success(t(dataForm.id ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,86 @@
<template>
<div class="layout-padding-auto layout-padding-view">
<div class="mb8">
<el-button icon="folder-add" type="primary" class="ml10" @click="dictformRef.openDialog(null, state.queryForm)">
{{ $t('common.addBtn') }}
</el-button>
<right-toolbar :search="false" class="ml10" style="float: right; margin-right: 20px" @queryTable="getDataList"></right-toolbar>
</div>
<el-table
:data="state.dataList"
v-loading="state.loading"
style="width: 100%"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column prop="dictType" :label="$t('dictItem.dictType')" show-overflow-tooltip></el-table-column>
<el-table-column prop="value" :label="$t('dictItem.itemValue')" show-overflow-tooltip></el-table-column>
<el-table-column prop="label" :label="$t('dictItem.label')" show-overflow-tooltip></el-table-column>
<el-table-column prop="description" :label="$t('dictItem.description')" show-overflow-tooltip></el-table-column>
<el-table-column prop="sortOrder" :label="$t('dictItem.sortOrder')" show-overflow-tooltip></el-table-column>
<el-table-column prop="remarks" :label="$t('dictItem.remarks')" show-overflow-tooltip></el-table-column>
<el-table-column prop="createTime" :label="$t('dictItem.createTime')" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" @click="dictformRef.openDialog(scope.row)"> {{ $t('common.editBtn') }} </el-button>
<el-button icon="delete" text type="primary" @click="handleDelete(scope.row)">
{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination"> </pagination>
<dict-form ref="dictformRef" @refresh="getDataList"></dict-form>
</div>
</template>
<script setup lang="ts" name="dict-item">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchItemList, delItemObj } from '/@/api/admin/dict';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const visible = ref(false);
const DictForm = defineAsyncComponent(() => import('./form.vue'));
const dictformRef = ref();
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
dictId: '',
dictType: '',
},
createdIsNeed: false,
pageList: fetchItemList,
});
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delItemObj(row.id);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
const open = (row: any) => {
state.queryForm.dictId = row.id;
state.queryForm.dictType = row.dictType;
visible.value = true;
getDataList();
};
// 暴露变量
defineExpose({
open,
});
</script>
<style scoped></style>

View File

@@ -0,0 +1,108 @@
<template>
<el-dialog :title="dataForm.id ? $t('common.editBtn') : $t('common.addBtn')" v-model="visible" width="600">
<el-form :model="dataForm" :rules="dataRules" label-width="100px" ref="dicDialogFormRef" v-loading="loading">
<el-form-item :label="$t('sysdict.systemFlag')" prop="systemFlag">
<el-radio-group v-model="dataForm.systemFlag">
<el-radio border :key="index" :label="item.value" v-for="(item, index) in dict_type">
{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('sysdict.dictType')" prop="dictType">
<el-input :placeholder="$t('sysdict.inputDictTypeTip')" :disabled="dataForm.id !== ''" clearable v-model="dataForm.dictType"></el-input>
</el-form-item>
<el-form-item :label="$t('sysdict.description')" prop="description">
<el-input :placeholder="$t('sysdict.inputDescriptionTip')" clearable v-model="dataForm.description"></el-input>
</el-form-item>
<el-form-item :label="$t('sysdict.remarks')" prop="remarks">
<el-input type="textarea" maxlength="100" :rows="3" :placeholder="$t('sysdict.inputRemarksTip')" v-model="dataForm.remarks"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="systemDicDialog" setup>
import { useI18n } from 'vue-i18n';
import { addObj, getObj, putObj, validateDictType } from '/@/api/admin/dict';
import { useDict } from '/@/hooks/dict';
import { useMessage } from '/@/hooks/message';
import { rule } from '/@/utils/validate';
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { dict_type } = useDict('dict_type');
const { t } = useI18n();
// 定义变量内容
const dicDialogFormRef = ref();
const visible = ref(false);
const loading = ref(false);
const dataForm = reactive({
id: '',
dictType: '',
description: '',
systemFlag: '0',
remarks: '',
});
const dataRules = reactive({
dictType: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '类型不能为空', trigger: 'blur' },
{ validator: rule.validatorNameCn, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateDictType(rule, value, callback, dataForm.id !== '');
},
trigger: 'blur',
},
],
systemFlag: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }],
description: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '描述不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
dataForm.id = '';
nextTick(() => {
dicDialogFormRef.value?.resetFields();
});
if (id) {
getObj(id).then((res) => {
Object.assign(dataForm, res.data);
});
}
};
// 提交
const onSubmit = async () => {
const valid = await dicDialogFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
const result = dataForm.id ? await putObj(dataForm) : await addObj(dataForm);
useMessage().success(t(dataForm.id ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh', result.data);
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,60 @@
export default {
sysdict: {
index: '#',
importsysDictTip: 'import SysDict',
id: 'id',
dictType: 'dictType',
description: 'description',
createBy: 'createBy',
updateBy: 'updateBy',
createTime: 'createTime',
updateTime: 'updateTime',
remarks: 'remarks',
systemFlag: 'systemFlag',
inputDictTypeTip: 'input dictType',
inputDescriptionTip: 'input description',
inputCreateByTip: 'input createBy',
inputUpdateByTip: 'input updateBy',
inputCreateTimeTip: 'input createTime',
inputUpdateTimeTip: 'input updateTime',
inputRemarksTip: 'input remarks',
inputSystemFlagTip: 'input systemFlag',
inputDelFlagTip: 'input delFlag',
inputTenantIdTip: 'input tenantId',
dictItem: 'dict item',
deleteDisabledTip: 'system data cannot be deleted ',
},
dictItem: {
index: '#',
name: 'dict item',
importsysDictItemTip: 'import SysDictItem',
id: 'id',
dictId: 'dictId',
itemValue: 'itemValue',
label: 'label',
dictType: 'dictType',
description: 'description',
sortOrder: 'sortOrder',
createBy: 'createBy',
updateBy: 'updateBy',
createTime: 'createTime',
updateTime: 'updateTime',
remarks: 'remarks',
delFlag: 'delFlag',
tenantId: 'tenantId',
inputIdTip: 'input id',
inputDictIdTip: 'input dictId',
inputItemValueTip: 'input itemValue',
inputLabelTip: 'input label',
inputDictTypeTip: 'input dictType',
inputDescriptionTip: 'input description',
inputSortOrderTip: 'input sortOrder',
inputCreateByTip: 'input createBy',
inputUpdateByTip: 'input updateBy',
inputCreateTimeTip: 'input createTime',
inputUpdateTimeTip: 'input updateTime',
inputRemarksTip: 'input remarks',
inputDelFlagTip: 'input delFlag',
inputTenantIdTip: 'input tenantId',
},
};

View File

@@ -0,0 +1,63 @@
export default {
sysdict: {
index: '#',
importsysDictTip: '导入字典表',
id: '编号',
dictType: '字典标识',
description: '字典名称',
createBy: '创建人',
updateBy: '修改人',
createTime: '创建时间',
updateTime: '更新时间',
remarks: '备注',
systemFlag: '配置类型',
delFlag: ' delFlag',
tenantId: '所属租户',
inputIdTip: '请输入编号',
inputDictTypeTip: '请输入字典类型',
inputDescriptionTip: '请输入描述',
inputCreateByTip: '请输入创建人',
inputUpdateByTip: '请输入修改人',
inputCreateTimeTip: '请输入创建时间',
inputUpdateTimeTip: '请输入更新时间',
inputRemarksTip: '请输入备注',
inputSystemFlagTip: '请输入字典类型',
inputDelFlagTip: '请输入 delFlag',
inputTenantIdTip: '请输入所属租户',
dictItem: '字典项',
deleteDisabledTip: '系统内置数据不能删除',
},
dictItem: {
index: '#',
name: '字典项',
importsysDictItemTip: '导入字典项',
id: '编号',
dictId: ' dictId',
itemValue: '数据值',
label: '标签名',
dictType: '类型',
description: '描述',
sortOrder: '排序',
createBy: '创建人',
updateBy: '修改人',
createTime: '创建时间',
updateTime: '更新时间',
remarks: '备注',
delFlag: ' delFlag',
tenantId: '所属租户',
inputIdTip: '请输入编号',
inputDictIdTip: '请输入 dictId',
inputItemValueTip: '请输入数据值',
inputLabelTip: '请输入标签名',
inputDictTypeTip: '请输入类型',
inputDescriptionTip: '请输入描述',
inputSortOrderTip: '请输入排序',
inputCreateByTip: '请输入创建人',
inputUpdateByTip: '请输入修改人',
inputCreateTimeTip: '请输入创建时间',
inputUpdateTimeTip: '请输入更新时间',
inputRemarksTip: '请输入备注',
inputDelFlagTip: '请输入 delFlag',
inputTenantIdTip: '请输入所属租户',
},
};

View File

@@ -0,0 +1,155 @@
<template>
<div class="layout-padding">
<splitpanes>
<pane class="ml10">
<splitpanes>
<pane size="30">
<div class="layout-padding-auto layout-padding-view">
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="dicDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain @click="handleRefreshCache()" class="ml10" icon="refresh-left" type="primary">
{{ $t('common.refreshCacheBtn') }}
</el-button>
</div>
</el-row>
<el-scrollbar>
<query-tree ref="dictTreeRef" :query="state.queryList" @node-click="handleNodeClick" placeholder="请输入字典项或名称">
<template #default="{ data }">
<span class="custom-tree-node">
<span class="label">{{ data.description }}</span>
<span class="code">{{ data.dictType }}</span>
<span class="do">
<el-button-group>
<el-button icon="el-icon-edit" size="small" @click.stop="dicDialogRef.openDialog(data.id)"></el-button>
<el-tooltip :content="$t('sysdict.deleteDisabledTip')" :disabled="data.systemFlag === '0'" placement="top">
<span style="margin-left: 12px">
<el-button
:disabled="data.systemFlag !== '0'"
icon="el-icon-delete"
size="small"
@click.stop="handleDelete([data.id])"
></el-button>
</span>
</el-tooltip>
</el-button-group>
</span>
</span>
</template>
</query-tree>
</el-scrollbar>
<el-footer style="height: 40px; line-height: 40px">
<el-button type="primary" size="small" icon="Download" style="width: 100%" @click="exportExcel">{{
$t('common.exportBtn')
}}</el-button>
</el-footer>
</div>
</pane>
<pane class="ml8">
<DicDialog @refresh="handleRefreshTree" ref="dicDialogRef" />
<dict-item-dialog ref="dictItemDialogRef"></dict-item-dialog>
</pane>
</splitpanes>
</pane>
</splitpanes>
</div>
</template>
<script lang="ts" name="systemDic" setup>
import { delObj, fetchList, refreshCache } from '/@/api/admin/dict';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
import { downBlobFile } from '/@/utils/other';
// 引入组件
const DicDialog = defineAsyncComponent(() => import('./form.vue'));
const DictItemDialog = defineAsyncComponent(() => import('./dictItem/index.vue'));
const QueryTree = defineAsyncComponent(() => import('/@/components/QueryTree/index.vue'));
const { t } = useI18n();
// 定义变量内容
const dicDialogRef = ref();
const dictTreeRef = ref();
const dictItemDialogRef = ref();
const state = reactive({
queryForm: {},
queryList: (name?: string) => {
return fetchList({
name: name,
});
},
});
// 导出EXCEL
const exportExcel = () => {
downBlobFile('/admin/dict/export', state.queryForm, 'dict.xlsx');
};
//刷新缓存
const handleRefreshCache = () => {
refreshCache().then(() => {
useMessage().success('同步成功');
});
};
// 点击树
const handleNodeClick = (data: any) => {
dictItemDialogRef.value.open(data);
};
// 刷新树
const handleRefreshTree = async (data: any) => {
await dictTreeRef.value.getdeptTree();
// 选择当前编辑、新增的节点
handleNodeClick(data);
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
useMessage().success(t('common.delSuccessText'));
dictTreeRef.value.getdeptTree();
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>
<style scoped>
.menu:deep(.el-tree-node__label) {
display: flex;
flex: 1;
height: 100%;
}
.custom-tree-node {
display: flex;
flex: 1;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 24px;
height: 100%;
}
.custom-tree-node .code {
font-size: 12px;
color: #999;
}
.custom-tree-node .do {
display: none;
}
.custom-tree-node:hover .code {
display: none;
}
.custom-tree-node:hover .do {
display: inline-block;
}
</style>

View File

@@ -0,0 +1,33 @@
export default {
file: {
index: '#',
importsysFileTip: 'import SysFile',
id: 'id',
fileName: 'fileName',
bucketName: 'bucketName',
original: 'original',
type: 'type',
fileSize: 'fileSize',
createBy: 'createBy',
updateBy: 'updateBy',
createTime: 'createTime',
updateTime: 'updateTime',
delFlag: 'delFlag',
tenantId: 'tenantId',
inputidTip: 'input id',
inputfileNameTip: 'input fileName',
inputbucketNameTip: 'input bucketName',
inputoriginalTip: 'input original',
inputtypeTip: 'input type',
inputfileSizeTip: 'input fileSize',
inputcreateByTip: 'input createBy',
inputupdateByTip: 'input updateBy',
inputcreateTimeTip: 'input createTime',
inputupdateTimeTip: 'input updateTime',
inputdelFlagTip: 'input delFlag',
inputtenantIdTip: 'input tenantId',
image: 'image',
video: 'video',
file: 'file',
},
};

View File

@@ -0,0 +1,32 @@
export default {
file: {
index: '#',
importsysFileTip: '导入文件管理表',
id: '编号',
fileName: '文件名称',
bucketName: '桶名称',
original: '原文件名',
type: '文件类型',
fileSize: '文件大小',
createBy: '创建人',
updateBy: '修改人',
createTime: '上传时间',
updateTime: '更新时间',
delFlag: '${field.fieldComment}',
tenantId: '所属租户',
inputidTip: '请输入编号',
inputfileNameTip: '请输入文件名称',
inputbucketNameTip: '请输入桶名称',
inputoriginalTip: '请输入原文件名',
inputtypeTip: '请输入文件类型',
inputfileSizeTip: '请输入文件大小',
inputcreateByTip: '请输入创建人',
inputupdateByTip: '请输入修改人',
inputcreateTimeTip: '请输入上传时间',
inputupdateTimeTip: '请输入更新时间',
inputtenantIdTip: '请输入所属租户',
image: '图片',
video: '视频',
file: '文件',
},
};

View File

@@ -0,0 +1,56 @@
<template>
<div class="layout-padding">
<div class="material-index">
<el-card class="!border-none" shadow="never">
<el-tabs v-model="activeTab">
<el-tab-pane v-for="item in tabsMap" :label="item.name" :name="item.type" :index="item.type" :key="item.type" lazy>
<material :type="item.type" mode="page" file-size="120px" :limit="-1" :page-size="20" />
</el-tab-pane>
</el-tabs>
</el-card>
</div>
</div>
</template>
<script lang="ts" setup name="fileCenter">
import { useI18n } from 'vue-i18n';
const Material = defineAsyncComponent(() => import('/@/components/Material/index.vue'));
const { t } = useI18n();
const tabsMap = [
{
type: 'image',
name: t('file.image'),
},
{
type: 'video',
name: t('file.video'),
},
{
type: 'file',
name: t('file.file'),
},
];
const activeTab = ref('image');
</script>
<style lang="scss" scoped>
.material-index {
min-width: 700px;
:deep(.el-tabs) {
height: calc(100vh - 180px);
display: flex;
flex-direction: column;
.el-tabs__header {
margin-bottom: 0 !important;
}
.el-tabs__content,
.el-tab-pane {
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
}
}
}
</style>

View File

@@ -0,0 +1,128 @@
<template>
<el-dialog :close-on-click-modal="false" width="600" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" draggable v-model="visible">
<el-form :model="form" :rules="dataRules" formDialogRef label-width="90px" ref="dataFormRef" v-loading="loading">
<el-form-item :label="t('i18n.name')" prop="name">
<el-input :placeholder="t('i18n.inputKeyTip')" v-model="form.name" />
</el-form-item>
<el-form-item :label="t('i18n.zhCn')" prop="zhCn">
<el-input :placeholder="t('i18n.inputZhCnTip')" v-model="form.zhCn" />
</el-form-item>
<el-form-item :label="t('i18n.en')" prop="en">
<el-input :placeholder="t('i18n.inputEnTip')" v-model="form.en" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="SysI18nDialog" setup>
// 定义子组件向父组件传值/事件
import { useMessage } from '/@/hooks/message';
import { addObj, getObj, putObj, validateName, validateZhCn, validateEn } from '/@/api/admin/i18n';
import { useI18n } from 'vue-i18n';
import { rule } from '/@/utils/validate';
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 提交表单数据
const form = reactive({
id: '',
name: '',
zhCn: '',
en: '',
});
// 定义校验规则
const dataRules = ref({
name: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: 'name不能为空', trigger: 'blur' },
{ validator: rule.noChinese, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateName(rule, value, callback, form.id !== '');
},
trigger: 'blur',
},
],
zhCn: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '中文不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateZhCn(rule, value, callback, form.id !== '');
},
trigger: 'blur',
},
],
en: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '英文不能为空', trigger: 'blur' },
{ validator: rule.letter, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateEn(rule, value, callback, form.id !== '');
},
trigger: 'blur',
},
],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.id = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取sysI18n信息
if (id) {
form.id = id;
getsysI18nData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
useMessage().success(t(form.id ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getsysI18nData = (id: string) => {
// 获取数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,24 @@
export default {
i18n: {
index: '#',
importsysI18nTip: 'import SysI18n',
id: 'id',
name: 'name',
zhCn: 'zh-cn',
en: 'en',
createBy: 'createBy',
createTime: 'createTime',
updateBy: 'updateBy',
updateTime: 'updateTime',
delFlag: 'delFlag',
inputIdTip: 'input id',
inputKeyTip: 'input key',
inputZhCnTip: 'input zh-cn',
inputEnTip: 'input en',
inputCreateByTip: 'input createBy',
inputCreateTimeTip: 'input createTime',
inputUpdateByTip: 'input updateBy',
inputUpdateTimeTip: 'input updateTime',
inputDelFlagTip: 'input delFlag',
},
};

View File

@@ -0,0 +1,24 @@
export default {
i18n: {
index: '#',
id: 'id',
name: '名称',
zhCn: '中文',
en: '英文',
createBy: '创建人',
createTime: '创建时间',
updateBy: '修改人',
updateTime: '更新时间',
delFlag: '删除标记',
inputIdTip: '请输入id',
inputKeyTip: '请输入key',
inputZhCnTip: '请输入中文',
inputEnTip: '请输入英文',
inputCreateByTip: '请输入创建人',
inputCreateTimeTip: '请输入创建时间',
inputUpdateByTip: '请输入修改人',
inputUpdateTimeTip: '请输入更新时间',
inputDelFlagTip: '请输入删除标记',
importsysI18nTip: '导入系统表-国际化',
},
};

View File

@@ -0,0 +1,167 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('i18n.name')" prop="name">
<el-input :placeholder="t('i18n.inputKeyTip')" style="max-width: 180px" v-model="state.queryForm.name" />
</el-form-item>
<el-form-item :label="$t('i18n.zhCn')" prop="zhCn">
<el-input :placeholder="t('i18n.inputZhCnTip')" style="max-width: 180px" v-model="state.queryForm.zhCn" />
</el-form-item>
<el-form-item :label="$t('i18n.en')" prop="en">
<el-input :placeholder="t('i18n.inputEnTip')" style="max-width: 180px" v-model="state.queryForm.en" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList" formDialogRef icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }} </el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="formDialogRef.openDialog()" class="ml10" formDialogRef icon="folder-add" type="primary" v-auth="'sys_i18n_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain @click="handleRefreshCache()" class="ml10" icon="refresh-left" type="primary">
{{ $t('common.refreshCacheBtn') }}
</el-button>
<el-button
:disabled="multiple"
@click="handleDelete(selectObjs)"
class="ml10"
formDialogRef
icon="Delete"
type="primary"
v-auth="'sys_i18n_del'"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'sys_i18n_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
style="width: 100%"
row-key="id"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="t('file.index')" type="index" width="60" />
<el-table-column :label="t('i18n.name')" prop="name" show-overflow-tooltip />
<el-table-column :label="t('i18n.zhCn')" prop="zhCn" show-overflow-tooltip />
<el-table-column :label="t('i18n.en')" prop="en" show-overflow-tooltip />
<el-table-column :label="t('i18n.createBy')" prop="createBy" show-overflow-tooltip />
<el-table-column :label="t('i18n.createTime')" prop="createTime" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" @click="formDialogRef.openDialog(scope.row.id)" text type="primary" v-auth="'sys_i18n_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" @click="handleDelete([scope.row.id])" text type="primary" v-auth="'sys_i18n_del'">
{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog @refresh="getDataList()" ref="formDialogRef" />
</div>
</template>
<script lang="ts" name="systemSysI18n" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchList, refreshCache } from '/@/api/admin/i18n';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义查询字典
// 定义变量内容
const formDialogRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
name: '',
zhCn: '',
en: '',
},
pageList: fetchList,
descs: ['create_time'],
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value.resetFields();
// 清空排序规则
state.queryForm!.descs = [];
state.queryForm!.ascs = [];
// 清空多选
selectObjs.value = [];
getDataList();
};
const handleRefreshCache = () => {
refreshCache().then(() => {
useMessage().success('同步成功');
});
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/i18n/export', Object.assign(state.queryForm,{ids:selectObjs}), 'i18n.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,100 @@
<template>
<el-drawer v-model="visible" :title="data.title" size="30%">
<div class="w-full">
<div class="coding inverse-toggle px-5 pt-4 shadow-lg text-gray-100 text-sm font-mono subpixel-antialiased
bg-gray-800 pb-6 pt-4 rounded-lg leading-normal overflow-hidden">
<div class="top mb-2 flex">
<div class="h-3 w-3 bg-red-500 rounded-full"></div>
<div class="ml-2 h-3 w-3 bg-orange-300 rounded-full"></div>
<div class="ml-2 h-3 w-3 bg-green-500 rounded-full"></div>
</div>
<div class="mt-4 flex">
<span class="text-green-400">{{ $t('syslog.createTime') }}: </span>
<p class="flex-1 typing items-center pl-2 whitespace-normal overflow-hidden break-all">
{{ data.createTime }}
<br>
</p>
</div>
<div class="mt-4 flex">
<span class="text-green-400">{{ $t('syslog.createBy') }}: </span>
<p class="flex-1 typing items-center pl-2 whitespace-normal overflow-hidden break-all">
{{ data.createBy }}
<br>
</p>
</div>
<div class="mt-4 flex">
<span class="text-green-400">{{ $t('syslog.requestUri') }}: </span>
<p class="flex-1 typing items-center pl-2 whitespace-normal overflow-hidden break-all">
{{ data.requestUri }}
<br>
</p>
</div>
<div class="mt-4 flex">
<span class="text-green-400">{{ $t('syslog.remoteAddr') }}: </span>
<p class="flex-1 typing items-center pl-2 whitespace-normal overflow-hidden break-all">
{{ data.remoteAddr }}
<br>
</p>
</div>
<div class="mt-4 flex">
<span class="text-green-400">{{ $t('syslog.method') }}: </span>
<p class="flex-1 typing items-center pl-2 whitespace-normal overflow-hidden break-all">
{{ data.method }}
<br>
</p>
</div>
<div class="mt-4 flex">
<span class="text-green-400">{{ $t('syslog.serviceId') }}: </span>
<p class="flex-1 typing items-center pl-2 whitespace-normal overflow-hidden break-all">
{{ data.serviceId }}
<br>
</p>
</div>
<div class="mt-4 flex">
<span class="text-green-400">{{ $t('syslog.time') }}: </span>
<p class="flex-1 typing items-center pl-2 whitespace-normal overflow-hidden break-all">
{{ data.time }}/ms
<br>
</p>
</div>
<div class="mt-4 flex">
<span class="text-green-400">{{ $t('syslog.ua') }}: </span>
<p class="flex-1 typing items-center pl-2 whitespace-normal overflow-hidden break-all">
{{ data.userAgent }}
<br>
</p>
</div>
<div class="mt-4 flex" v-if="data.params">
<span class="text-green-400">{{ $t('syslog.params') }}: </span>
<p class="flex-1 typing items-center pl-2 whitespace-normal overflow-hidden break-all">
{{ data.params }}
<br>
</p>
</div>
<div class="mt-4 flex" v-if="data.exception">
<span class="text-green-400">{{ data.logType === '0' ? $t('syslog.result') : $t('syslog.exception') }}: </span>
<p class="flex-1 typing items-center pl-2 whitespace-normal overflow-hidden break-all">
{{ data.exception }}
<br>
</p>
</div>
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts" name="log-detail">
const visible = ref(false);
const data = reactive({} as any);
const openDialog = (row: any) => {
visible.value = true;
Object.assign(data, row);
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,22 @@
export default {
syslog: {
index: '#',
logType: 'logType',
title: 'title',
remoteAddr: 'remoteAddr',
method: 'method',
ua: 'browser',
serviceId: 'serviceId',
time: 'time',
params: 'params',
createTime: 'createTime',
requestUri: 'requestUri',
exception: 'exception',
createBy: 'createBy',
action: 'action',
inputLogTypeTip: 'select logType',
inputStartPlaceholderTip: 'Start Time',
inputEndPlaceholderTip: 'End TIme',
result: 'result'
},
};

View File

@@ -0,0 +1,22 @@
export default {
syslog: {
index: '#',
logType: '类型',
title: '标题',
remoteAddr: 'IP地址',
method: '请求方式',
ua: '浏览器',
serviceId: '客户端',
time: '耗时',
params: '请求参数',
createTime: '请求时间',
requestUri: '请求地址',
exception: '异常信息',
createBy: '操作人',
action: '操作',
inputLogTypeTip: '请选择类型',
inputStartPlaceholderTip: '开始时间',
inputEndPlaceholderTip: '结束时间',
result: '结果',
},
};

View File

@@ -0,0 +1,202 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<!-- 顶部折线图-->
<log-line-chart/>
<el-row class="mt-4 ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('syslog.logType')" prop="logType">
<el-select :placeholder="$t('syslog.inputLogTypeTip')" clearable
v-model="state.queryForm.logType">
<el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in log_type"/>
</el-select>
</el-form-item>
<el-form-item :label="$t('syslog.createTime')" prop="createTime">
<el-date-picker
:end-placeholder="$t('syslog.inputEndPlaceholderTip')"
:start-placeholder="$t('syslog.inputStartPlaceholderTip')"
range-separator="To"
type="datetimerange"
v-model="state.queryForm.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="Search" type="primary">{{ $t('common.queryBtn') }}</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb-2" style="width: 100%">
<el-button :disabled="multiple" v-auth="'sys_log_del'" @click="handleDelete(selectObjs)" class="ml10"
icon="Delete" type="primary">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'sys_log_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
ref="tableRef"
:data="state.dataList"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40"/>
<el-table-column :label="$t('syslog.index')" type="index" width="60"/>
<el-table-column :label="$t('syslog.logType')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="log_type" :value="scope.row.logType"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="$t('syslog.title')" prop="title" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('syslog.remoteAddr')" prop="remoteAddr" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('syslog.method')" prop="method" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('syslog.time')" prop="time" show-overflow-tooltip>
<template #default="scope">
<span v-if="scope.row.time">{{ scope.row.time }}/ms</span>
</template>
</el-table-column>
<el-table-column :label="$t('syslog.createTime')" prop="createTime" show-overflow-tooltip sortable="custom"
width="200"></el-table-column>
<el-table-column :label="$t('syslog.createBy')" prop="createBy" show-overflow-tooltip sortable="custom"
width="200"></el-table-column>
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="view" @click="LogDetailRef.openDialog(scope.row)" size="small" text type="primary">
{{ $t('common.detailBtn') }}
</el-button>
<el-button v-auth="'sys_log_del'" icon="delete" @click="handleDelete([scope.row.id])" size="small" text
type="primary">
{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle"
v-bind="state.pagination"></pagination>
<log-detail ref="LogDetailRef"></log-detail>
</div>
</div>
</template>
<script lang="ts" setup>
import {BasicTableProps, useTable} from '/@/hooks/table';
import {delObj, pageList} from '/@/api/admin/log';
import {useI18n} from 'vue-i18n';
import {useMessage, useMessageBox} from '/@/hooks/message';
import {useDict} from '/@/hooks/dict';
const LogDetail = defineAsyncComponent(() => import('./detail.vue'));
const LogLineChart = defineAsyncComponent(() => import('./line-chart.vue'));
const LogDetailRef = ref();
const {log_type} = useDict('log_type');
const {t} = useI18n();
// 定义变量内容
const queryRef = ref();
const showSearch = ref(true);
// 多选rows
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
let tableRef = ref(null);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
logType: '',
createTime: '',
serviceId: '',
},
selectObjs: [],
pageList: pageList,
descs: ['create_time'],
createdIsNeed: false,
});
// table hook
const {
downBlobFile,
getDataList,
currentChangeHandle: baseCurrentChangeHandle,
sortChangeHandle,
sizeChangeHandle,
tableStyle
} = useTable(state);
// 分页事件
const currentChangeHandle = (page: number) => {
// Reset table scroll position to top
tableRef.value?.setScrollTop(0);
// Call the original handler
baseCurrentChangeHandle(page);
};
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields();
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/log/export', state.queryForm, 'log.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({id}) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
// onMounted 通过路由参数给 serviceId 赋值
const route = useRoute();
onMounted(() => {
const {serviceId} = route.query;
if (serviceId) {
state.queryForm.serviceId = serviceId;
}
getDataList();
});
</script>
<style lang="scss" scoped>
pre code.hljs {
width: 65%;
}
</style>

View File

@@ -0,0 +1,202 @@
<template>
<v-chart class="w-full h-80" :option="option" />
</template>
<script setup lang="ts" name="log-line-chart">
import VChart from 'vue-echarts';
import { formatPast } from '/@/utils/formatTime';
import { getSum } from '/@/api/admin/log';
import { use } from 'echarts/core';
import { LineChart } from 'echarts/charts';
import { GridComponent, LegendComponent, TitleComponent, ToolboxComponent, TooltipComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
use([TitleComponent, TooltipComponent, LegendComponent, ToolboxComponent, GridComponent, LineChart, CanvasRenderer]);
const option = reactive({
title: {
textStyle: {
fontSize: 16,
fontWeight: 500,
color: '#303133',
},
padding: [20, 0, 0, 20],
},
tooltip: {
trigger: 'axis',
backgroundColor: '#ffffff',
borderRadius: 8,
padding: [12, 16],
borderWidth: 0,
shadowColor: 'rgba(0, 0, 0, 0.1)',
shadowBlur: 12,
shadowOffsetY: 4,
textStyle: {
color: '#303133',
},
axisPointer: {
type: 'line',
lineStyle: {
color: '#ebeef5',
width: 1,
type: 'dashed',
},
},
},
legend: {
data: ['成功', '失败'],
icon: 'circle',
itemWidth: 8,
itemHeight: 8,
textStyle: {
color: '#606266',
fontSize: 12,
},
right: '20px',
top: '20px',
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '15%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [],
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: '#909399',
fontSize: 12,
margin: 16,
},
splitLine: {
show: true,
lineStyle: {
color: '#ebeef5',
type: 'dashed',
},
},
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
color: '#ebeef5',
type: 'dashed',
},
},
axisLabel: {
color: '#909399',
fontSize: 12,
margin: 16,
},
axisLine: {
show: false,
},
axisTick: {
show: false,
},
},
series: [
{
name: '成功',
type: 'line',
stack: 'Total',
data: [],
smooth: true,
symbol: 'circle',
symbolSize: 8,
itemStyle: {
color: '#79bbff',
borderColor: '#fff',
borderWidth: 2,
},
lineStyle: {
width: 3,
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(121, 187, 255, 0.2)',
},
{
offset: 1,
color: 'rgba(121, 187, 255, 0.02)',
},
],
},
},
},
{
name: '失败',
type: 'line',
stack: 'x',
data: [],
smooth: true,
symbol: 'circle',
symbolSize: 8,
itemStyle: {
color: '#909399',
borderColor: '#fff',
borderWidth: 2,
},
lineStyle: {
width: 3,
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(144, 147, 153, 0.2)',
},
{
offset: 1,
color: 'rgba(144, 147, 153, 0.02)',
},
],
},
},
},
],
});
interface LogSumItem {
createTime: string;
'0'?: number;
'9'?: number;
}
onMounted(() => {
getSum().then((res) => {
option.xAxis.data = res.data.map((item: LogSumItem) => formatPast(new Date(item.createTime), 'mm-dd'));
option.series[0].data = res.data.map((item: LogSumItem) => item['0'] || 0);
option.series[1].data = res.data.map((item: LogSumItem) => item['9'] || 0);
});
});
</script>
<style scoped>
:deep(.echarts) {
background: transparent;
}
</style>

View File

@@ -0,0 +1,171 @@
<template>
<el-dialog :close-on-click-modal="false" :title="form.publicId ? $t('common.editBtn') : $t('common.addBtn')" draggable v-model="visible">
<el-form :model="form" :rules="dataRules" formDialogRef label-width="90px" ref="dataFormRef" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="t('param.publicName')" prop="publicName">
<el-input :placeholder="t('param.inputpublicNameTip')" v-model="form.publicName" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('param.publicKey')" prop="publicKey">
<el-input :placeholder="t('param.inputpublicKeyTip')" v-model="form.publicKey" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('param.publicValue')" prop="publicValue">
<el-input :placeholder="t('param.inputpublicValueTip')" v-model="form.publicValue" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('param.status')" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="item.value" border v-for="(item, index) in status_type" :key="index">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('param.validateCode')" prop="validateCode">
<el-input :placeholder="t('param.inputvalidateCodeTip')" v-model="form.validateCode" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('param.publicType')" prop="publicType">
<el-select :placeholder="t('param.inputpublicTypeTip')" v-model="form.publicType">
<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in param_type"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('param.systemFlag')" prop="systemFlag">
<el-radio-group v-model="form.systemFlag">
<el-radio :label="item.value" border v-for="(item, index) in dict_type" :key="index">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="SysPublicParamDialog" setup>
// 定义子组件向父组件传值/事件
import { useDict } from '/@/hooks/dict';
import { useMessage } from '/@/hooks/message';
import { addObj, getObj, putObj, validateParamsCode, validateParamsName } from '/@/api/admin/param';
import { useI18n } from 'vue-i18n';
import { rule } from '/@/utils/validate';
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 定义字典
const { dict_type, status_type, param_type } = useDict('dict_type', 'status_type', 'param_type');
// 提交表单数据
const form = reactive({
publicId: '',
publicName: '',
publicKey: '',
publicValue: '',
status: '0',
validateCode: '',
publicType: '0',
systemFlag: '0',
});
// 定义校验规则
const dataRules = reactive({
publicName: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '名称不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateParamsName(rule, value, callback, form.publicId !== '');
},
trigger: 'blur',
},
],
publicKey: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '参数键不能为空', trigger: 'blur' },
{ validator: rule.validatorCapital, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateParamsCode(rule, value, callback, form.publicId !== '');
},
trigger: 'blur',
},
],
publicValue: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '参数值不能为空', trigger: 'blur' }],
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
publicType: [{ required: true, message: '类型不能为空', trigger: 'blur' }],
systemFlag: [{ required: true, message: '类型不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.publicId = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取sysPublicParam信息
if (id) {
form.publicId = id;
getsysPublicParamData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
form.publicId ? await putObj(form) : await addObj(form);
useMessage().success(t(form.publicId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getsysPublicParamData = (id: string) => {
// 获取数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,34 @@
export default {
param: {
index: '#',
importsysPublicParamTip: 'import SysPublicParam',
publicId: 'publicId',
publicName: 'publicName',
publicKey: 'publicKey',
publicValue: 'publicValue',
status: 'status',
validateCode: 'validateCode',
createBy: 'createBy',
updateBy: 'updateBy',
createTime: 'createTime',
updateTime: 'updateTime',
publicType: 'publicType',
systemFlag: 'systemFlag',
delFlag: 'delFlag',
tenantId: 'tenantId',
inputpublicIdTip: 'input publicId',
inputpublicNameTip: 'input publicName',
inputpublicKeyTip: 'input publicKey',
inputpublicValueTip: 'input publicValue',
inputstatusTip: 'input status',
inputvalidateCodeTip: 'input validateCode',
inputcreateByTip: 'input createBy',
inputupdateByTip: 'input updateBy',
inputcreateTimeTip: 'input createTime',
inputupdateTimeTip: 'input updateTime',
inputpublicTypeTip: 'input publicType',
inputsystemFlagTip: 'input systemFlag',
inputdelFlagTip: 'input delFlag',
inputtenantIdTip: 'input tenantId',
},
};

View File

@@ -0,0 +1,32 @@
export default {
param: {
index: '#',
importsysPublicParamTip: '导入公共参数配置表',
publicId: '编号',
publicName: '名称',
publicKey: '键',
publicValue: '值',
status: '状态',
validateCode: '编码',
createBy: '创建人',
updateBy: '修改人',
createTime: '创建时间',
updateTime: '修改时间',
publicType: '类型',
systemFlag: '类型',
tenantId: '租户ID',
inputpublicIdTip: '请输入编号',
inputpublicNameTip: '请输入名称',
inputpublicKeyTip: '请输入键',
inputpublicValueTip: '请输入值',
inputstatusTip: '请输入状态',
inputvalidateCodeTip: '请输入编码',
inputcreateByTip: '请输入创建人',
inputupdateByTip: '请输入修改人',
inputcreateTimeTip: '请输入创建时间',
inputupdateTimeTip: '请输入修改时间',
inputpublicTypeTip: '请输入类型',
inputsystemFlagTip: '请输入类型',
inputtenantIdTip: '请输入租户ID',
},
};

View File

@@ -0,0 +1,192 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" ref="queryRef">
<el-form-item :label="$t('param.publicName')" prop="publicName">
<el-input :placeholder="$t('param.inputpublicNameTip')" style="max-width: 180px" v-model="state.queryForm.publicName" />
</el-form-item>
<el-form-item :label="$t('param.publicKey')" prop="publicKey">
<el-input :placeholder="$t('param.inputpublicKeyTip')" style="max-width: 180px" v-model="state.queryForm.publicKey" />
</el-form-item>
<el-form-item :label="t('param.systemFlag')" class="ml2" prop="systemFlag">
<el-select :placeholder="t('param.inputsystemFlagTip')" v-model="state.queryForm.systemFlag">
<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in dict_type"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="getDataList" formDialogRef icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" formDialogRef icon="Refresh">{{ $t('common.resetBtn') }} </el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'sys_syspublicparam_add'" @click="formDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain v-auth="'sys_syspublicparam_del'" @click="handleRefreshCache()" class="ml10" icon="refresh-left" type="primary">
{{ $t('common.refreshCacheBtn') }}
</el-button>
<el-button
plain
v-auth="'sys_syspublicparam_del'"
:disabled="multiple"
@click="handleDelete(selectObjs)"
class="ml10"
icon="Delete"
type="primary"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'sys_syspublicparam_del'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
style="width: 100%"
row-key="publicId"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column :selectable="handleSelectable" align="center" type="selection" width="40" />
<el-table-column :label="t('param.index')" type="index" width="60" />
<el-table-column :label="t('param.publicName')" prop="publicName" show-overflow-tooltip />
<el-table-column :label="t('param.publicKey')" prop="publicKey" show-overflow-tooltip />
<el-table-column :label="t('param.publicValue')" prop="publicValue" show-overflow-tooltip />
<el-table-column :label="t('param.status')" prop="status" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="status_type" :value="scope.row.status"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="t('param.createTime')" prop="createTime" show-overflow-tooltip />
<el-table-column :label="t('param.systemFlag')" prop="systemFlag" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dict_type" :value="scope.row.systemFlag"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" @click="formDialogRef.openDialog(scope.row.publicId)" text type="primary"
>{{ $t('common.editBtn') }}
</el-button>
<el-tooltip :content="$t('sysdict.deleteDisabledTip')" :disabled="scope.row.systemFlag === '0'" placement="top">
<span style="margin-left: 12px">
<el-button
icon="delete"
v-auth="'sys_syspublicparam_del'"
:disabled="scope.row.systemFlag !== '0'"
@click="handleDelete([scope.row.publicId])"
text
type="primary"
>
{{ $t('common.delBtn') }}
</el-button>
</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog @refresh="getDataList()" ref="formDialogRef" />
</div>
</template>
<script lang="ts" name="systemSysPublicParam" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchList, refreshCache } from '/@/api/admin/param';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
import { useI18n } from 'vue-i18n';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义查询字典
const { dict_type, status_type } = useDict('dict_type', 'status_type');
// 定义变量内容
const formDialogRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
systemFlag: '',
},
pageList: fetchList,
descs: ['create_time'],
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 是否可以多选
const handleSelectable = (row: any) => {
// 系统类不可多选删除
return row.systemFlag !== '1';
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/param/export', Object.assign(state.queryForm,{ids:selectObjs}), 'param.xlsx');
};
const handleRefreshCache = () => {
refreshCache().then(() => {
useMessage().success('同步成功');
});
};
// 多选事件
const handleSelectionChange = (objs: { publicId: string }[]) => {
selectObjs.value = objs.map(({ publicId }) => publicId);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,131 @@
<template>
<el-dialog :title="form.sensitiveId ? '编辑' : '新增'" v-model="visible"
:close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="敏感词" prop="sensitiveWord">
<el-input v-model="form.sensitiveWord" placeholder="请输入敏感词"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="类型" prop="sensitiveType">
<el-radio-group v-model="form.sensitiveType">
<el-radio :label="item.value" v-for="(item, index) in sensitive_type" border :key="index">{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="SysSensitiveWordDialog">
import {useDict} from '/@/hooks/dict';
import {useMessage} from "/@/hooks/message";
import {getObj, addObj, putObj, validateWord} from '/@/api/admin/sensitive'
import {rule} from '/@/utils/validate';
const emit = defineEmits(['refresh']);
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false)
const loading = ref(false)
// 定义字典
const {sensitive_type} = useDict('sensitive_type')
// 提交表单数据
const form = reactive({
sensitiveId: '',
sensitiveWord: '',
sensitiveType: '0',
remark: '',
});
// 定义校验规则
const dataRules = ref({
sensitiveWord: [{validator: rule.overLength, trigger: 'blur'}
, {
validator: (rule: any, value: any, callback: any) => {
validateWord(rule, value, callback, form.sensitiveId !== '');
}
, trigger: 'blur'
}
, {
required: true,
message: '敏感词不能为空',
trigger: 'blur'
}],
sensitiveType: [{required: true, message: '类型不能为空', trigger: 'blur'}],
remark: [{validator: rule.overLength, trigger: 'blur'}],
})
// 打开弹窗
const openDialog = (sensitiveId: string) => {
visible.value = true
form.sensitiveId = ''
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取sysSensitiveWord信息
if (sensitiveId) {
form.sensitiveId = sensitiveId
getsysSensitiveWordData(sensitiveId)
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {
});
if (!valid) return false;
try {
loading.value = true;
form.sensitiveId ? await putObj(form) : await addObj(form);
useMessage().success(form.sensitiveId ? '修改成功' : '添加成功');
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getsysSensitiveWordData = (sensitiveId: string) => {
// 获取数据
loading.value = true
getObj({sensitiveId}).then((res: any) => {
Object.assign(form, res.data)
}).finally(() => {
loading.value = false
})
};
// 暴露变量
defineExpose({
openDialog
});
</script>

View File

@@ -0,0 +1,165 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item label="敏感词" prop="sensitiveWord">
<el-input placeholder="请输入敏感词" v-model="state.queryForm.sensitiveWord"/>
</el-form-item>
<el-form-item label="类型" prop="sensitiveType">
<el-radio-group v-model="state.queryForm.sensitiveType">
<el-radio :label="item.value" v-for="(item, index) in sensitive_type" border :key="index">{{ item.label }}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">
查询
</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()"
v-auth="'admin_sysSensitiveWord_add'">
</el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary"
v-auth="'admin_sysSensitiveWord_del'" @click="handleDelete(selectObjs)">
</el-button>
<el-button plain icon="Check" type="primary"
v-auth="'admin_sysSensitiveWord_del'" @click="matchDialogRef.openDialog()">
匹配测试
</el-button>
<el-button plain @click="handleRefreshCache()" class="ml10" icon="refresh-left" type="primary">
{{ $t('common.refreshCacheBtn') }}
</el-button>
<right-toolbar v-model:showSearch="showSearch" :export="'admin_sysSensitiveWord_export'"
@exportExcel="exportExcel" class="ml10 mr20" style="float: right;"
@queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table :data="state.dataList" v-loading="state.loading" border
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
@sort-change="sortChangeHandle">
<el-table-column type="selection" width="40" align="center"/>
<el-table-column type="index" label="#" width="40"/>
<el-table-column prop="sensitiveWord" label="敏感词" show-overflow-tooltip/>
<el-table-column prop="sensitiveType" label="类型" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="sensitive_type" :value="scope.row.sensitiveType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" width="200" show-overflow-tooltip/>
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" v-auth="'admin_sysSensitiveWord_edit'"
@click="formDialogRef.openDialog(scope.row.sensitiveId)">编辑
</el-button>
<el-button icon="delete" text type="primary" v-auth="'admin_sysSensitiveWord_del'"
@click="handleDelete([scope.row.sensitiveId])">删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination"/>
</div>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)"/>
<!-- 匹配测试 -->
<match-dialog ref="matchDialogRef" @refresh="getDataList(false)"/>
</div>
</template>
<script setup lang="ts" name="systemSysSensitiveWord">
import {BasicTableProps, useTable} from "/@/hooks/table";
import {fetchList, delObjs, refreshObj} from "/@/api/admin/sensitive";
import {useMessage, useMessageBox} from "/@/hooks/message";
import {useDict} from '/@/hooks/dict';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const MatchDialog = defineAsyncComponent(() => import('./match.vue'));
// 定义查询字典
const {sensitive_type} = useDict('sensitive_type')
// 定义变量内容
const formDialogRef = ref()
const matchDialogRef = ref()
// 搜索变量
const queryRef = ref()
const showSearch = ref(true)
// 多选变量
const selectObjs = ref([]) as any
const multiple = ref(true)
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
downBlobFile,
tableStyle
} = useTable(state)
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields()
// 清空多选
selectObjs.value = []
getDataList()
}
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/sysSensitiveWord/export', Object.assign(state.queryForm, {ids: selectObjs}), 'sysSensitiveWord.xlsx')
}
// 多选事件
const selectionChangHandle = (objs: { sensitiveId: string }[]) => {
selectObjs.value = objs.map(({sensitiveId}) => sensitiveId);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm('此操作将永久删除');
} catch {
return;
}
try {
await delObjs(ids);
getDataList();
useMessage().success('删除成功');
} catch (err: any) {
useMessage().error(err.msg);
}
};
// 刷新缓存
const handleRefreshCache = async () => {
try {
await refreshObj();
getDataList();
useMessage().success('刷新成功');
} catch (err: any) {
useMessage().error(err.msg);
}
}
</script>

View File

@@ -0,0 +1,127 @@
<template>
<el-dialog title="匹配测试" v-model="visible"
:close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item label="敏感词" prop="sensitiveWord">
<el-input type="textarea" rows="3" v-model="form.sensitiveWord" placeholder="请输入敏感词"
@blur="onSubmit"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item prop="result">
<template #label>
匹配结果
<tip content="可点击敏感词加入白名单"/>
</template>
<div v-html="matchResult" @click="handleChildClick"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="SysSensitiveWordDialog">
import {useMessage} from "/@/hooks/message";
import {testObj, addObj, getObj} from '/@/api/admin/sensitive'
import {rule} from '/@/utils/validate';
const emit = defineEmits(['refresh']);
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false)
const loading = ref(false)
const matchResult = ref('')
// 提交表单数据
const form = reactive({
sensitiveId: '',
sensitiveWord: '',
});
// 定义校验规则
const dataRules = ref({
sensitiveWord: [{validator: rule.overLength, trigger: 'blur'}, {
required: true,
message: '敏感词不能为空',
trigger: 'blur'
}]
})
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true
form.sensitiveId = ''
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
};
const handleChildClick = async (event: any) => {
try {
if (event.target.tagName.toLowerCase() === 'a' && event.target.classList.contains('link-error')) {
const {data} = await getObj({sensitiveWord: event.target.innerText, sensitiveType: '1'})
if (data) {
useMessage().error('数据已存在,请勿重新添加');
return
}
await addObj({sensitiveWord: event.target.innerText, sensitiveType: '1'})
useMessage().success('白名单添加成功');
emit('refresh');
}
} catch (err: any) {
useMessage().error(err.msg);
}
}
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {
});
if (!valid) return false;
try {
loading.value = true;
const {data} = await testObj(form);
// 要处理的字符串
matchResult.value = data;
// 遍历关键词数组,并进行替换
matchResult.value = matchResult.value.map((item: string) => {
let modifiedItem = item;
data.forEach((word: string) => {
let regex = new RegExp(word, 'g');
modifiedItem = modifiedItem.replace(regex, `
<div class="tooltip tooltip-open tooltip-bottom" data-tip="触发敏感词">
<a class="link link-error" @click="$emit('click-child','${word}')">${word}</a>
</div>
`);
});
return modifiedItem;
});
useMessage().success('操作成功');
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 暴露变量
defineExpose({
openDialog
});
</script>

View File

@@ -0,0 +1,150 @@
<template>
<el-dialog :close-on-click-modal="false" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" draggable v-model="visible">
<el-form :model="form" :rules="dataRules" formDialogRef label-width="90px" ref="dataFormRef" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="t('social.type')" prop="type">
<el-select :placeholder="t('social.inputTypeTip')" v-model="form.type">
<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in social_type">
{{ item.label }}
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('social.remark')" prop="remark">
<el-input :placeholder="t('social.inputRemarkTip')" v-model="form.remark" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('social.appId')" prop="appId">
<el-input :placeholder="t('social.inputAppIdTip')" v-model="form.appId" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('social.appSecret')" prop="appSecret">
<el-input :placeholder="t('social.inputAppSecretTip')" v-model="form.appSecret" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('social.redirectUrl')" prop="redirectUrl">
<el-input :placeholder="t('social.inputRedirectUrlTip')" type="textarea" v-model="form.redirectUrl" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('social.ext')" prop="ext">
<el-input :placeholder="t('social.inputExtTip')" type="textarea" v-model="form.ext" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="AppSocialDetailsDialog" setup>
// 定义子组件向父组件传值/事件
import { useDict } from '/@/hooks/dict';
import { useMessage } from '/@/hooks/message';
import { addObj, getObj, putObj } from '/@/api/admin/social';
import { useI18n } from 'vue-i18n';
import { rule } from '/@/utils/validate';
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 定义字典
const { social_type } = useDict('social_type');
// 提交表单数据
const form = reactive({
id: '',
type: '',
remark: '',
appId: '' as string | undefined,
appSecret: '' as string | undefined,
redirectUrl: '',
ext: '',
});
// 定义校验规则
const dataRules = ref({
type: [{ required: true, message: '类型不能为空', trigger: 'blur' }],
appId: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: 'appId不能为空', trigger: 'blur' }],
remark: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '描述不能为空', trigger: 'blur' }],
redirectUrl: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '回调地址不能为空', trigger: 'blur' },
{ validator: rule.url, trigger: 'blur' },
],
appSecret: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: 'appSecret不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.id = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取appSocialDetails信息
if (id) {
form.id = id;
getappSocialDetailsData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
// 隐藏敏感信息
form.appSecret = form.appSecret?.includes('******') ? undefined : form.appSecret;
form.appId = form.appId?.includes('******') ? undefined : form.appId;
try {
loading.value = true;
if (form.id) {
await putObj(form);
useMessage().success(t('common.editSuccessText'));
} else {
await addObj(form);
useMessage().success(t('common.addSuccessText'));
}
visible.value = false; // 关闭弹窗
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getappSocialDetailsData = (id: string) => {
// 获取数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,32 @@
export default {
social: {
index: '#',
importappSocialDetailsTip: 'import AppSocialDetails',
id: 'id',
type: 'type',
remark: 'remark',
appId: 'appId',
appSecret: 'appSecret',
redirectUrl: 'redirectUrl',
ext: 'ext',
createBy: 'createBy',
updateBy: 'updateBy',
createTime: 'createTime',
updateTime: 'updateTime',
delFlag: 'delFlag',
tenantId: 'tenantId',
inputIdTip: 'input id',
inputTypeTip: 'input type',
inputRemarkTip: 'input remark',
inputAppIdTip: 'input appId',
inputAppSecretTip: 'input appSecret',
inputRedirectUrlTip: 'input redirectUrl',
inputExtTip: 'input ext',
inputCreateByTip: 'input createBy',
inputUpdateByTip: 'input updateBy',
inputCreateTimeTip: 'input createTime',
inputUpdateTimeTip: 'input updateTime',
inputDelFlagTip: 'input delFlag',
inputTenantIdTip: 'input tenantId',
},
};

View File

@@ -0,0 +1,32 @@
export default {
social: {
index: '#',
importappSocialDetailsTip: '导入系统社交登录账号表',
id: '主鍵',
type: '类型',
remark: '描述',
appId: 'appId',
appSecret: 'appSecret',
redirectUrl: '回调地址',
ext: '拓展字段',
createBy: '创建人',
updateBy: '修改人',
createTime: '创建时间',
updateTime: '更新时间',
delFlag: '${field.fieldComment}',
tenantId: '所属租户',
inputIdTip: '请输入主鍵',
inputTypeTip: '请输入类型',
inputRemarkTip: '请输入描述',
inputAppIdTip: '请输入appId',
inputAppSecretTip: '请输入appSecret',
inputRedirectUrlTip: '请输入回调地址',
inputExtTip: '请输入拓展字段',
inputCreateByTip: '请输入创建人',
inputUpdateByTip: '请输入修改人',
inputCreateTimeTip: '请输入创建时间',
inputUpdateTimeTip: '请输入更新时间',
inputDelFlagTip: '请输入${field.fieldComment}',
inputTenantIdTip: '请输入所属租户',
},
};

View File

@@ -0,0 +1,148 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form ref="queryRef" :inline="true" :model="state.queryForm">
<el-form-item :label="t('social.type')" class="ml2" prop="type">
<el-select v-model="state.queryForm.type" :placeholder="t('social.inputTypeTip')">
<el-option v-for="(item, index) in social_type" :key="index" :label="item.label" :value="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button formDialogRef icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'sys_social_details_add'" class="ml10" icon="folder-add" type="primary" @click="formDialogRef.openDialog()">
{{ $t('common.addBtn') }}
</el-button>
<el-button
v-auth="'sys_social_details_del'"
:disabled="multiple"
class="ml10"
icon="Delete"
type="primary"
@click="handleDelete(selectObjs)"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'sys_social_details_del'"
@exportExcel="exportExcel"
v-model:showSearch="showSearch"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
v-loading="state.loading"
:data="state.dataList"
style="width: 100%"
row-key="id"
@selection-change="handleSelectionChange"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="t('social.index')" type="index" width="60" />
<el-table-column :label="t('social.type')" prop="type" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="social_type" :value="scope.row.type"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="t('social.remark')" prop="remark" show-overflow-tooltip />
<el-table-column :label="t('social.appId')" prop="appId" show-overflow-tooltip />
<el-table-column :label="t('social.appSecret')" prop="appSecret" show-overflow-tooltip />
<el-table-column :label="t('social.createTime')" prop="createTime" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" v-auth="'sys_social_details_edit'" text type="primary" @click="formDialogRef.openDialog(scope.row.id)"
>{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" v-auth="'sys_social_details_del'" text type="primary" @click="handleDelete([scope.row.id])"
>{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle" />
</div>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList()" />
</div>
</template>
<script lang="ts" name="systemAppSocialDetails" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchList } from '/@/api/admin/social';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
import { useI18n } from 'vue-i18n';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义查询字典
const { social_type } = useDict('social_type');
// 定义变量内容
const formDialogRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
descs: ['create_time'],
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/social/export', Object.assign(state.queryForm,{ids:selectObjs}), 'social.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,194 @@
<template>
<el-dialog :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" v-model="visible"
:close-on-click-modal="false" draggable width="600">
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
<el-row>
<el-col :span="24" class="mb20" v-if="!form.id">
<el-form-item :label="t('area.pid')" prop="pid">
<china-area class="w-full" :placeholder="t('area.inputPidByTip')" :plus="true" @change="handleChange"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('area.name')" prop="name">
<el-input v-model="form.name" :placeholder="t('area.inputNameByTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('area.adcode')" prop="adcode">
<el-input-number v-model="form.adcode" :placeholder="t('area.inputAdCodeByTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('area.areaType')" prop="areaType">
<el-select v-model="form.areaType">
<el-option :key="item.value" :label="item.label" :value="item.value"
v-for="item in area_type_dict"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('area.areaSort')" prop="areaSort">
<el-input-number v-model="form.areaSort" :placeholder="t('area.inputAreaSortByTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('area.hot')" prop="hot">
<el-radio-group v-model="form.hot">
<el-radio :key="index" :label="item.value" border v-for="(item, index) in yes_no_type">{{
item.label
}}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('area.areaStatus')" prop="areaStatus">
<el-radio-group v-model="form.areaStatus">
<el-radio :key="index" :label="item.value" border v-for="(item, index) in yes_no_type">{{
item.label
}}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="SysAreaDialog">
import {useMessage} from "/@/hooks/message";
import {getObj, addObj, putObj, validateExist} from '/@/api/admin/sysArea'
import {useDict} from "/@/hooks/dict";
import {useI18n} from "vue-i18n";
import {rule} from "/@/utils/validate";
const ChinaArea = defineAsyncComponent(() => import("/@/components/ChinaArea/index.vue"));
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false)
const loading = ref(false)
// 定义字典
const {yes_no_type} = useDict('yes_no_type')
const area_type_dict = [
{value: '0', label: '国家'},
{value: '1', label: '省份'},
{value: '2', label: '城市'},
{value: '3', label: '县区'},
{value: '4', label: '街道'}
]
// 提交表单数据
const form = reactive({
id: '',
pid: 100000,
name: '',
letter: '',
adcode: 0,
location: '',
areaSort: 0,
areaStatus: '1',
areaType: '2',
hot: '0',
cityCode: '',
});
// 定义校验规则
const dataRules = ref({
name: [
{required: true, message: '地区名称不能为空', trigger: 'blur'},
{min: 2, max: 20, message: '长度在 3 到 30 个字符', trigger: 'blur'},
{
validator: (rule: any, value: any, callback: any) => {
validateExist(rule, value, callback, form.id !== '');
},
trigger: 'blur',
},
],
adcode: [
{ validator: rule.overLength, trigger: 'blur' },
{required: true, message: '编码不能为空', trigger: 'blur'},
{
validator: (rule: any, value: any, callback: any) => {
validateExist(rule, value, callback, form.id !== '');
},
trigger: 'blur',
},
]
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true
form.id = ''
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取sysArea信息
if (id) {
form.id = id
getsysAreaData(id)
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {
});
if (!valid) return false;
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
useMessage().success(form.id ? t('common.editSuccessText') : t('common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getsysAreaData = (id: string) => {
// 获取数据
loading.value = true
getObj({id: id}).then((res: any) => {
Object.assign(form, res.data)
}).finally(() => {
loading.value = false
})
};
// 地区选择
const handleChange = (data: string) => {
let dataArray = data.split(",");
form.pid = dataArray[dataArray.length - 1];
}
// 暴露变量
defineExpose({
openDialog
});
</script>

View File

@@ -0,0 +1,17 @@
export default {
area: {
index: '#',
id: 'id',
pid: 'pid',
name: 'name',
adcode:'adcode',
areaType:'areaType',
areaSort:'areaSort',
hot:'hot',
areaStatus:'areaStatus',
inputAdCodeByTip: 'input adcode',
inputPidByTip: 'input pid',
inputNameByTip: 'input name',
inputAreaSortByTip: 'input sort',
},
};

View File

@@ -0,0 +1,17 @@
export default {
area: {
index: '#',
id: '主键',
pid: '父级地区',
name: '名称',
adcode:'编码',
areaType:'类型',
areaSort:'排序值',
hot:'热门',
areaStatus:'有效',
inputAdCodeByTip: '请选择编码',
inputPidByTip: '请选择父级地区',
inputNameByTip: '请输入地区名称',
inputAreaSortByTip: '请输入排序值',
},
};

View File

@@ -0,0 +1,174 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item :label="t('area.pid')" prop="adcode">
<china-area :type="3" :placeholder="t('area.inputPidByTip')" v-model="pid" :plus="true" @change="handleChange"/>
</el-form-item>
<el-form-item :label="t('area.name')" prop="name">
<el-input :placeholder="t('area.inputNameByTip')" v-model="state.queryForm.name"/>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()"
v-auth="'sys_sysArea_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary"
v-auth="'sys_sysArea_del'" @click="handleDelete(selectObjs)">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar v-model:showSearch="showSearch" :export="'sys_sysArea_export'"
@exportExcel="exportExcel" class="ml10 mr20" style="float: right;"
@queryTable="getDataList"></right-toolbar>
</div>
</el-row>
<el-table :data="state.dataList" v-loading="state.loading" border
:cell-style="tableStyle.cellStyle" :header-cell-style="tableStyle.headerCellStyle"
@selection-change="selectionChangHandle"
@sort-change="sortChangeHandle">
<el-table-column type="selection" width="40" align="center"/>
<el-table-column type="index" label="#" width="40"/>
<el-table-column prop="name" :label="t('area.name')" show-overflow-tooltip/>
<el-table-column prop="adcode" :label="t('area.adcode')" show-overflow-tooltip/>
<el-table-column prop="areaType" :label="t('area.areaType')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="area_type_dict" :value="scope.row.areaType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="hot" :label="t('area.hot')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="yes_no_type" :value="scope.row.hot"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="areaStatus" :label="t('area.areaStatus')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="yes_no_type" :value="scope.row.areaStatus"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="areaSort" :label="t('area.areaSort')" width="100" sortable="custom"
show-overflow-tooltip/>
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" v-auth="'sys_sysArea_edit'"
@click="formDialogRef.openDialog(scope.row.id)">{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" text type="primary" v-auth="'sys_sysArea_del'"
@click="handleDelete([scope.row.id])">{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination"/>
</div>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)"/>
</div>
</template>
<script setup lang="ts" name="systemSysArea">
import {BasicTableProps, useTable} from "/@/hooks/table";
import {delObjs, fetchList} from "/@/api/admin/sysArea";
import {useMessage, useMessageBox} from "/@/hooks/message";
import {useDict} from '/@/hooks/dict';
import {useI18n} from "vue-i18n";
const {t} = useI18n();
// 省市区查询组件
const ChinaArea = defineAsyncComponent(() => import("/@/components/ChinaArea/index.vue"));
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
// 定义查询字典
const {yes_no_type} = useDict('yes_no_type');
const area_type_dict = [
{value: '0', label: '国家'},
{value: '1', label: '省份'},
{value: '2', label: '城市'},
{value: '3', label: '县区'},
{value: '4', label: '街道'}
]
// 定义变量内容
const formDialogRef = ref()
// 搜索变量
const queryRef = ref()
const showSearch = ref(true)
// 多选变量
const selectObjs = ref([]) as any
const multiple = ref(true)
const pid = ref()
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
adcode: '',
name: ''
},
ascs: ['adcode'],
pageList: fetchList
})
// table hook
const {
getDataList,
currentChangeHandle,
sizeChangeHandle,
sortChangeHandle,
downBlobFile,
tableStyle
} = useTable(state)
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields()
pid.value = ''
// 清空多选
selectObjs.value = []
getDataList()
}
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/sysArea/export', Object.assign(state.queryForm, {ids: selectObjs}), 'sysArea.xlsx')
}
// 多选事件
const selectionChangHandle = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({id}) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObjs(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
// 地区查询
const handleChange = (data: string) => {
let dataArray = data.split(",");
state.queryForm.adcode = dataArray[dataArray.length - 1];
}
</script>

View File

@@ -0,0 +1,118 @@
<template>
<el-dialog :title="dataForm.deptId ? $t('common.editBtn') : $t('common.addBtn')" v-model="visible" width="600">
<el-form ref="deptDialogFormRef" :model="dataForm" label-width="90px" :rules="dataRules" v-loading="loading">
<el-form-item :label="$t('sysdept.parentId')" prop="parentId">
<el-tree-select
v-model="dataForm.parentId"
:data="parentData"
:props="{ value: 'id', label: 'name', children: 'children' }"
class="w100"
clearable
check-strictly
:render-after-expand="false"
:placeholder="$t('sysdept.inputparentIdTip')"
/>
</el-form-item>
<el-form-item :label="$t('sysdept.name')" prop="name">
<el-input v-model="dataForm.name" :placeholder="$t('sysdept.inputnameTip')" clearable />
</el-form-item>
<el-form-item :label="$t('sysdept.sortOrder')" prop="sortOrder">
<el-input-number v-model="dataForm.sortOrder" :placeholder="$t('sysdept.inputsortOrderTip')" clearable />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="systemDeptDialog">
import { useI18n } from 'vue-i18n';
import { getObj, deptTree, addObj, putObj } from '/@/api/admin/dept';
import { useMessage } from '/@/hooks/message';
import {rule} from "/@/utils/validate";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const deptDialogFormRef = ref();
const dataForm = reactive({
parentId: '',
deptId: '',
name: '',
sortOrder: 9999,
});
const parentData = ref<any[]>([]);
const visible = ref(false);
const loading = ref(false);
const dataRules = ref({
parentId: [{ required: true, message: '上级部门不能为空', trigger: 'blur' }],
name: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '部门名称不能为空', trigger: 'blur' }],
sortOrder: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (type: string, id: string) => {
visible.value = true;
dataForm.deptId = '';
nextTick(() => {
deptDialogFormRef.value?.resetFields();
dataForm.parentId = id;
});
if (type === 'edit') {
getObj(id)
.then((res) => {
Object.assign(dataForm, res.data);
})
.catch((err) => {
useMessage().error(err.msg);
});
}
getDeptData();
};
// 提交
const onSubmit = async () => {
const valid = await deptDialogFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
dataForm.deptId ? await putObj(dataForm) : await addObj(dataForm);
useMessage().success(t(dataForm.deptId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 从后端获取菜单信息
const getDeptData = async () => {
deptTree().then((res) => {
parentData.value = [];
const dept = {
id: '0',
name: '根部门',
children: [] as any[],
};
dept.children = res.data;
parentData.value.push(dept);
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,21 @@
export default {
sysdept: {
name: 'dept name',
parentId: 'parent dept',
createTime: 'createTime',
weight: 'weight',
leaderId: 'dept leader',
sortOrder: 'sortOrder',
inputdeptNameTip: 'input deptName',
inputnameTip: 'input deptName',
inputparentIdTip: 'select deptName',
inputLeaderIdTip: 'input leader',
inputsortOrderTip: 'input sortOrder',
importTip: 'import dept',
addNodeText:'add dept',
editNodeText:'edit dept',
delNodeText:'delete dept',
view: 'tree/table view',
tenantNodeErrorText: 'The current node cannot be operated. You need to maintain it in tenant management',
},
};

View File

@@ -0,0 +1,21 @@
export default {
sysdept: {
name: '部门名称',
parentId: '上级部门',
createTime: '创建时间',
weight: '排序',
sortOrder: '排序',
leaderId: '部门负责人',
inputdeptNameTip: '请输入部门名称',
inputnameTip: '请输入部门名称',
inputLeaderIdTip: '请输入部门领导',
inputparentIdTip: '请选择上级部门',
inputsortOrderTip: '请输入排序',
importTip: '导入部门',
addNodeText: '添加部门',
editNodeText: '编辑部门',
delNodeText: '删除部门',
tenantNodeErrorText: '当前节点不可操作,请在租户管理功能中维护',
view: '树/表视图'
},
};

View File

@@ -0,0 +1,130 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="filter">
<el-form-item prop="deptName" :label="$t('sysdept.name')">
<el-input :placeholder="$t('sysdept.inputdeptNameTip')" style="max-width: 180px" v-model="state.queryForm.deptName"></el-input>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="filter">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="top-right-btn" v-if="!defaultTreeViewRef" v-auth="'sys_dept_add'" @click="handleAdd">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain icon="upload-filled" type="primary" class="ml10" @click="excelUploadRef.show()">
{{ $t('common.importBtn') }}
</el-button>
<el-button @click="handleExpand"> {{ $t('common.expandBtn') }}</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_dept_add'"
@exportExcel="exportExcel"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
>
<el-tooltip class="item" effect="dark" :content="$t('queryTree.view')" placement="top">
<el-button circle icon="Grid" @click="handleView"></el-button>
</el-tooltip>
</right-toolbar>
</div>
</el-row>
<tree-view ref="treeViewRef" v-if="defaultTreeViewRef" />
<table-view ref="tableViewRef" v-if="!defaultTreeViewRef" />
<upload-excel
ref="excelUploadRef"
:title="$t('sysdept.importTip')"
url="/admin/dept/import"
temp-url="/admin/sys-file/local/file/dept.xlsx"
@refreshDataList="getDataList"
/>
</div>
</div>
</template>
<script lang="ts" name="systemDept" setup>
import { downBlobFile } from '/@/utils/other';
import { useI18n } from 'vue-i18n';
const TreeView = defineAsyncComponent(() => import('./tree-view.vue'));
const TableView = defineAsyncComponent(() => import('./table-view.vue'));
const { t } = useI18n();
// 默认树视图展示
const defaultTreeViewRef = ref(true);
const treeViewRef = ref();
const tableViewRef = ref();
const excelUploadRef = ref();
const showSearch = ref(true);
const isExpand = ref(false);
const queryRef = ref();
const state = reactive({
queryForm: {
deptName: '',
},
});
/**
* 过滤节点
*/
const filter = () => {
if (defaultTreeViewRef.value) {
treeViewRef.value.filter(state.queryForm.deptName);
} else {
tableViewRef.value.state.queryForm.deptName = state.queryForm.deptName;
tableViewRef.value.getDataList();
}
};
const handleAdd = () => {
tableViewRef.value.handleAdd();
};
const handleView = () => {
defaultTreeViewRef.value = !defaultTreeViewRef.value;
};
/**
* 处理展开/折叠树
*/
const handleExpand = () => {
if (defaultTreeViewRef.value) {
treeViewRef.value.handleExpand();
} else {
tableViewRef.value.handleExpand();
}
};
/**
* 导出Excel
*/
const exportExcel = () => {
downBlobFile('/admin/dept/export', state.queryForm, 'dept.xlsx');
};
const getDataList = () => {
if (defaultTreeViewRef.value) {
treeViewRef.value.getOrgData();
} else {
tableViewRef.value.getDataList();
}
};
/**
* 重置查询条件
*/
const resetQuery = () => {
queryRef.value.resetFields();
};
</script>

View File

@@ -0,0 +1,137 @@
<template>
<el-table
ref="tableRef"
:data="state.dataList"
v-loading="state.loading"
style="width: 100%"
row-key="id"
default-expand-all
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle?.headerCellStyle"
>
<el-table-column :label="$t('sysdept.name')" prop="name" width="400" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysdept.weight')" prop="weight" show-overflow-tooltip width="80"></el-table-column>
<el-table-column prop="createTime" :label="$t('sysdept.createTime')" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" show-overflow-tooltip width="250">
<template #default="scope">
<el-button text type="primary" icon="folder-add" @click="deptDialogRef.openDialog('add', scope.row?.id)"
v-auth="'sys_dept_add'">
{{ $t('common.addBtn') }}
</el-button
>
<el-button text type="primary" icon="edit-pen" @click="deptDialogRef.openDialog('edit', scope.row?.id)"
v-auth="'sys_dept_edit'">{{
$t('common.editBtn')
}}
</el-button>
<el-button text type="primary" icon="delete" @click="handleDelete(scope.row)" v-auth="'sys_dept_del'">
{{ $t('common.delBtn') }}
</el-button
>
</template>
</el-table-column>
</el-table>
<dept-form ref="deptDialogRef" @refresh="getDataList()"/>
</template>
<script setup lang="ts" name="systemDept">
import {BasicTableProps, useTable} from '/@/hooks/table';
import {deptTree, delObj} from '/@/api/admin/dept';
import {useMessage, useMessageBox} from '/@/hooks/message';
import {useI18n} from 'vue-i18n';
// 引入组件
const DeptForm = defineAsyncComponent(() => import('./form.vue'));
const {t} = useI18n();
// 定义变量内容
const tableRef = ref(); // 表格引用
const deptDialogRef = ref(); // 部门对话框引用
const excelUploadRef = ref(); // Excel上传引用
const showSearch = ref(true); // 是否显示搜索栏
const isExpand = ref(false); // 是否展开
/**
* 查询部门树方法,返回 Promise 对象
* @param params - 查询参数
* @returns Promise&lt;any&gt;
*/
const queryDeptTree = (params?: any) => {
return deptTree(params);
};
/**
* 定义响应式表格数据
*/
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: queryDeptTree, // 页面列表数据
queryForm: {
deptName: '', // 部门名称
},
isPage: false, // 是否分页
descs: ['create_time'], // 排序字段
});
/**
* 使用 useTable 定义表格相关操作
*/
const {getDataList, tableStyle} = useTable(state);
/**
* 展开/折叠部门树方法
*/
const handleExpand = async () => {
isExpand.value = !isExpand.value;
const dataList = await deptTree();
toggleExpand(dataList.data, isExpand.value);
};
/**
* 递归方法,用于展开/折叠部门树
* @param children - 子节点
* @param unfold - 是否展开
*/
const toggleExpand = (children: any[], unfold = true) => {
for (const key in children) {
tableRef.value?.toggleRowExpansion(children[key], unfold);
if (children[key].children) {
toggleExpand(children[key].children!, unfold);
}
}
};
/**
* 删除当前行
* @param row - 当前行数据
*/
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(row.id);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
const handleAdd = ()=>{
deptDialogRef.value.openDialog('add')
}
/**
* 暴露组件中的一些方法和变量
*/
defineExpose({
handleAdd, // 新增时间
state, // 响应式表格数据
getDataList, // 获取列表数据方法
handleExpand // 展开/折叠部门树方法
});
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div style="height: 100%">
<vue3-tree-org
ref="treeOrgRef"
:props="props"
:data="data"
:label-style="style"
:define-menus="defineMenus"
:default-expand-level="expandLevel"
center
:horizontal="horizontal"
:collapsable="collapsable"
:only-one-node="onlyOneNode"
:filter-node-method="filterNodeMethod"
:clone-node-drag="cloneNodeDrag"
:node-add="addNode"
:node-delete="delNode"
:node-edit="editNode"
@on-node-click="onNodeClick"
/>
</div>
<dept-form ref="deptDialogRef" @refresh="getOrgData()" />
</template>
<script lang="ts" name="treeView" setup>
import { useMessage, useMessageBox } from '/@/hooks/message';
import { delObj, deptTree } from '/@/api/admin/dept';
import { getObj } from '/@/api/admin/tenant';
import { Session } from '/@/utils/storage';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
const DeptForm = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义org组件key-value定义
const props = reactive({ id: 'id', pid: 'parentId', label: 'name', expand: 'expand', children: 'children' });
const data = reactive({});
// 定义org组件右键定义
const defineMenus = reactive([
{ name: t('sysdept.addNodeText'), command: 'add' },
{ name: t('sysdept.editNodeText'), command: 'edit' },
{ name: t('sysdept.delNodeText'), command: 'delete' },
]);
const cloneNodeDrag = ref(true);
const collapsable = ref(false);
const expandLevel = ref(2); //默认展开层级
const horizontal = ref(false);
const onlyOneNode = ref(false);
const treeOrgRef = ref();
const deptDialogRef = ref();
// 添加主题配置
const themeConfig = useThemeConfig();
const { themeConfig: theme } = storeToRefs(themeConfig);
// 添加 style 定义
const style = computed(() => ({
background: theme.value.isDark ? 'var(--el-bg-color-overlay)' : 'var(--el-bg-color-page)',
color: theme.value.isDark ? 'var(--el-text-color-primary)' : '#5e6d82',
}));
/**
* 过滤节点
*/
const filter = (deptName: string) => {
treeOrgRef.value.filter(deptName);
};
/**
* 节点过滤方法
* @param {string} value 过滤条件
* @param {object} data 节点数据
* @returns {boolean} 返回过滤结果
*/
const filterNodeMethod = (value, data) => {
if (!value) return true;
return data.label.indexOf(value) !== -1;
};
/**
* 处理展开/折叠树
*/
const handleExpand = async () => {
collapsable.value = !collapsable.value;
};
/**
* 检查节点是否是根租户节点
* @param {object} node 节点对象
* @returns {boolean} 如果节点是根租户节点返回true否则返回false
*/
const checkNode = (node) => {
if (node?.id === '0') {
useMessage().error(t('sysdept.tenantNodeErrorText'));
return false;
}
return true;
};
/**
* 当用户左键点击节点,模拟触发组件的右键事件
* @param e
*/
const onNodeClick = (e: any) => {
const { clientX, clientY } = e;
const rightFun = new MouseEvent('contextmenu', {
bubbles: true,
cancelable: false,
view: window,
button: 2,
buttons: 0,
clientX,
clientY,
});
e.target.dispatchEvent(rightFun);
};
/**
* 添加部门
* @param {object} node 节点对象
*/
const addNode = (node) => {
deptDialogRef.value.openDialog('add', node?.id);
};
/**
* 编辑部门
* @param {object} node 节点对象
*/
const editNode = (node) => {
if (!checkNode(node)) {
return;
}
deptDialogRef.value.openDialog('edit', node?.id);
};
/**
* 删除部门
* @param {object} node 节点对象
*/
const delNode = async (node) => {
if (!checkNode(node)) {
return;
}
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(node.id);
await getOrgData();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
/**
* 查询部门数据
*/
const getOrgData = async () => {
// 查询当前租户信息
const tenant = await getObj(Session.getTenant());
deptTree().then((res) => {
Object.assign(data, { id: '0', name: tenant.data.name });
data.children = res.data;
});
};
onMounted(() => {
getOrgData();
});
// 暴露变量
defineExpose({
getOrgData,
handleExpand,
filter,
});
</script>
<style lang="scss" scoped>
:deep(.zm-tree-org) {
height: 100%;
padding: 15px;
position: relative;
background: var(--el-bg-color);
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,260 @@
<template>
<el-dialog
:title="state.ruleForm.menuId ? $t('common.editBtn') : $t('common.addBtn')"
v-model="visible"
width="600"
:close-on-click-modal="false"
draggable
>
<el-form ref="menuDialogFormRef" :model="state.ruleForm" :rules="dataRules" label-width="100px" v-loading="loading">
<el-form-item :label="$t('sysmenu.menuType')" prop="menuType">
<el-radio-group v-model="state.ruleForm.menuType">
<el-radio border label="0">菜单</el-radio>
<el-radio border label="1">按钮</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="$t('sysmenu.parentId')" prop="parentId">
<el-tree-select
v-model="state.ruleForm.parentId"
:data="state.parentData"
:render-after-expand="false"
:props="{ value: 'id', label: 'name', children: 'children' }"
class="w100"
clearable
check-strictly
:placeholder="$t('sysmenu.inputParentIdTip')"
>
</el-tree-select>
</el-form-item>
<el-form-item prop="name">
<template #label>
{{ state.ruleForm.menuType === '0' ? t('sysmenu.name') : t('sysmenu.buttonName') }}
</template>
<el-input v-model="state.ruleForm.name" clearable :placeholder="$t('sysmenu.inputNameTip')"></el-input>
</el-form-item>
<el-form-item :label="$t('sysmenu.path')" prop="path" v-if="state.ruleForm.menuType === '0'">
<el-input v-model="state.ruleForm.path" :placeholder="$t('sysmenu.inputPathTip')"/>
</el-form-item>
<el-form-item :label="$t('sysmenu.permission')" prop="permission" v-if="state.ruleForm.menuType === '1'">
<template #label>
{{ t('sysmenu.permission') }}
<tip content="对应后台接口@PreAuthorize注解入参字符串"></tip>
</template>
<el-input v-model="state.ruleForm.permission" maxlength="30" :placeholder="$t('sysmenu.inputPermissionTip')"/>
</el-form-item>
<el-form-item :label="$t('sysmenu.sortOrder')" prop="sortOrder">
<el-input-number v-model="state.ruleForm.sortOrder" :min="0" controls-position="right"/>
</el-form-item>
<el-form-item :label="$t('sysmenu.icon')" prop="icon" v-if="state.ruleForm.menuType === '0'">
<IconSelector :placeholder="$t('sysmenu.inputIconTip')" v-model="state.ruleForm.icon"/>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item prop="keepAlive" v-if="state.ruleForm.menuType === '0'">
<template #label> {{ $t('sysmenu.keepAlive') }}
<tip content="组件保留状态,避免重新渲染"/>
</template>
<el-radio-group v-model="state.ruleForm.keepAlive">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="visible" v-if="state.ruleForm.menuType === '0'">
<template #label> {{ $t('sysmenu.visible') }}
<tip content="左侧菜单树是否显示"/>
</template>
<el-radio-group v-model="state.ruleForm.visible">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row class="mt-4">
<el-col :span="12">
<el-form-item prop="param" v-if="state.ruleForm.menuType === '0'">
<template #label> {{ $t('sysmenu.param') }}
<tip content="多个路径指向同一个组件"/>
</template>
<el-radio-group v-model="state.ruleForm.param">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="embedded"
v-if="state.ruleForm.menuType === '0' && state.ruleForm.path?.startsWith('http')">
<template #label> {{ $t('sysmenu.embedded') }}
<tip content="iframe嵌套还是打开独立的Tab"/>
</template>
<el-radio-group v-model="state.ruleForm.embedded">
<el-radio border label="0"></el-radio>
<el-radio border label="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item class="mt-4" :label="$t('sysmenu.component')" prop="component" v-if="state.ruleForm.menuType === '0'
&& state.ruleForm.param === '1'">
<el-input v-model="state.ruleForm.component" :placeholder="$t('sysmenu.inputComponentTip')"/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="systemMenuDialog">
import {useI18n} from 'vue-i18n';
import {getObj, pageList, putObj, addObj, validateExist} from '/@/api/admin/menu';
import {useMessage} from '/@/hooks/message';
import {rule, validateNull} from "/@/utils/validate";
import Tip from "/@/components/Tip/index.vue";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const {t} = useI18n();
// 引入组件
const IconSelector = defineAsyncComponent(() => import('/@/components/IconSelector/index.vue'));
// 定义变量内容
const visible = ref(false);
const loading = ref(false);
const menuDialogFormRef = ref();
// 定义需要的数据
const state = reactive({
ruleForm: {
menuId: '',
name: '',
permission: '',
parentId: '',
icon: '',
path: '',
param: '0',
component: '',
sortOrder: 0,
menuType: '1',
keepAlive: '0',
visible: '1',
embedded: '0',
},
parentData: [] as any[], // 上级菜单数据
});
// 表单校验规则
const dataRules = reactive({
menuType: [{required: true, message: '菜单类型不能为空', trigger: 'blur'}],
parentId: [{required: true, message: '上级菜单不能为空', trigger: 'blur'}],
name: [{validator: rule.overLength, trigger: 'blur'}, {
required: true,
message: '菜单不能为空',
trigger: 'blur'
}, {
validator: (rule: any, value: any, callback: any) => {
validateExist(rule, value, callback, state.ruleForm.menuId !== '' || state.ruleForm.menuType === '1');
},
trigger: 'blur',
}],
path: [{validator: rule.overLength, trigger: 'blur'}, {required: true, message: '路径不能为空', trigger: 'blur'}],
icon: [{validator: rule.overLength, trigger: 'blur'}, {required: true, message: '图标不能为空', trigger: 'blur'}],
permission: [{validator: rule.overLength, trigger: 'blur'}, {
required: true,
message: '权限代码不能为空',
trigger: 'blur'
}, {
validator: (rule: any, value: any, callback: any) => {
validateExist(rule, value, callback, state.ruleForm.menuId !== '');
},
trigger: 'blur',
}],
sortOrder: [{required: true, message: '排序不能为空', trigger: 'blur'}],
component: [{min: 5, max: 255, message: '组件名称长度必须介于 5 和 255 之间', trigger: 'blur'},
{
validator: (rule: any, value: any, callback: any) => {
if (state.ruleForm.menuType === '0' && state.ruleForm.param === '1' && validateNull(state.ruleForm.component)) {
callback(new Error('请输入组件名称'));
} else {
return callback();
}
},
trigger: 'blur',
}],
});
// 打开弹窗
const openDialog = (type: string, row?: any) => {
state.ruleForm.menuId = '';
visible.value = true;
nextTick(() => {
menuDialogFormRef.value?.resetFields();
state.ruleForm.parentId = row?.id || '-1';
});
if (row?.id && type === 'edit') {
state.ruleForm.menuId = row.id;
// 获取当前节点菜单信息
getMenuDetail(row.id);
}
// 渲染上级菜单列表树
getAllMenuData();
};
// 获取菜单节点的详细信息
const getMenuDetail = (id: string) => {
getObj({menuId: id}).then((res) => {
if (res.data[0].component) {
state.ruleForm.param = '1'
}
Object.assign(state.ruleForm, res.data[0]);
});
};
// 从后端获取菜单信息(含层级)
const getAllMenuData = () => {
state.parentData = [];
pageList({
type: '0',
}).then((res) => {
let menu = {
id: '-1',
name: '根菜单',
children: [],
};
menu.children = res.data;
state.parentData.push(menu);
});
};
// 保存数据
const onSubmit = async () => {
const valid = await menuDialogFormRef.value.validate().catch(() => {
});
if (!valid) return false;
try {
loading.value = true;
state.ruleForm.menuId ? await putObj(state.ruleForm) : await addObj(state.ruleForm);
useMessage().success(t(state.ruleForm.menuId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 暴露变量 只有暴漏出来的变量 父组件才能使用
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,37 @@
export default {
sysmenu: {
index: '#',
name: 'menu name',
buttonName: 'button name',
sortOrder: 'sortOrder',
path: 'path',
menuType: 'menuType',
keepAlive: 'keepAlive',
permission: 'permission',
inputNameTip: 'input name',
parentId: 'parent menu',
embedded: 'embedded',
param: 'param',
component: 'component',
visible: 'visible',
icon: 'icon',
inputMenuIdTip: 'input menuId',
inputPermissionTip: 'input permission',
inputPathTip: 'input path',
inputParentIdTip: 'input parentId',
inputIconTip: 'input icon',
inputVisibleTip: 'input visible',
inputSortOrderTip: 'input sortOrder',
inputKeepAliveTip: 'input keepAlive',
inputMenuTypeTip: 'input menuType',
inputCreateByTip: 'input createBy',
inputCreateTimeTip: 'input createTime',
inputUpdateByTip: 'input updateBy',
inputUpdateTimeTip: 'input updateTime',
inputDelFlagTip: 'input delFlag',
inputTenantIdTip: 'input tenantId',
inputEmbeddedTip: 'input embedded',
inputComponentTip: 'input component',
deleteDisabledTip: 'menu inclusion subordinates cannot be deleted',
},
};

View File

@@ -0,0 +1,31 @@
export default {
sysmenu: {
index: '#',
name: '菜单名称',
buttonName: '按钮名称',
sortOrder: '排序',
path: '路由',
menuType: '类型',
keepAlive: '缓冲',
permission: '权限标识',
inputNameTip: '请输入菜单名称',
parentId: '上级菜单',
embedded: '内嵌',
param: '带参',
component: '组件',
visible: '显示',
icon: '图标',
inputMenuIdTip: '',
inputPermissionTip: '请输入权限标识',
inputPathTip: '请输入路由路径',
inputParentIdTip: '请选择上级菜单',
inputIconTip: '请选择图标',
inputVisibleTip: '请选择是否显示',
inputSortOrderTip: '请输入排序',
inputKeepAliveTip: '请选择是否缓冲',
inputMenuTypeTip: '请选择菜单类型',
inputEmbeddedTip: '请选择是否内嵌',
inputComponentTip: '请输入组件名称',
deleteDisabledTip: '菜单包含下级不能删除',
},
};

View File

@@ -0,0 +1,192 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('sysmenu.name')" prop="menuName">
<el-input :placeholder="$t('sysmenu.inputNameTip')" clearable style="max-width: 180px" v-model="state.queryForm.menuName" />
</el-form-item>
<el-form-item>
<el-button @click="query" class="ml10" icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="onOpenAddMenu" class="ml10" icon="folder-add" type="primary" v-auth="'sys_menu_add'">
{{ $t('common.addBtn') }}
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
ref="tableRef"
:data="tableList"
lazy
:load="load"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
row-key="path"
style="width: 100%"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle?.headerCellStyle"
>
<el-table-column :label="$t('sysmenu.name')" fixed prop="name" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysmenu.sortOrder')" prop="sortOrder" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysmenu.icon')" prop="icon" show-overflow-tooltip>
<template #default="scope">
<SvgIcon :name="scope.row.meta.icon" />
</template>
</el-table-column>
<el-table-column :label="$t('sysmenu.path')" prop="path" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysmenu.menuType')" show-overflow-tooltip>
<template #default="scope">
<el-tag v-if="scope.row.menuType === '0'">左菜单</el-tag>
<el-tag v-if="scope.row.menuType === '2'">顶菜单</el-tag>
<el-tag type="success" v-if="scope.row.menuType === '1'">按钮</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('sysmenu.keepAlive')" show-overflow-tooltip>
<template #default="scope">
<el-tag v-if="scope.row.meta.isKeepAlive">开启</el-tag>
<el-tag type="info" v-else>关闭</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('sysmenu.permission')" :show-overflow-tooltip="true" prop="permission"></el-table-column>
<el-table-column :label="$t('common.action')" show-overflow-tooltip width="250">
<template #default="scope">
<el-button icon="folder-add" @click="onOpenAddMenu('add', scope.row)" text type="primary" v-auth="'sys_menu_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button icon="edit-pen" @click="onOpenEditMenu('edit', scope.row)" text type="primary" v-auth="'sys_menu_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-tooltip icon="delete" :content="$t('sysmenu.deleteDisabledTip')" :disabled="!deleteMenuDisabled(scope.row)" placement="top">
<span style="margin-left: 12px">
<el-button
icon="delete"
:disabled="deleteMenuDisabled(scope.row)"
@click="handleDelete(scope.row)"
text
type="primary"
v-auth="'sys_menu_del'"
>
{{ $t('common.delBtn') }}
</el-button>
</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<MenuDialog @refresh="query()" ref="menuDialogRef" />
</div>
</template>
<script lang="ts" name="systemMenu" setup>
import { delObj, pageList } from '/@/api/admin/menu';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 引入组件
const MenuDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义变量内容
const tableRef = ref();
const menuDialogRef = ref();
const showSearch = ref(true);
const queryRef = ref();
const state: BasicTableProps = reactive<BasicTableProps>({
pageList: pageList, // H
queryForm: {
parentId: -1,
menuName: '',
},
isPage: false,
});
const { getDataList, tableStyle } = useTable(state);
// 根据类型判断是否有子节点
const setHasChildren = (arr: any[]) => {
arr.forEach((item) => {
// 添加 hasChildren 属性
item.hasChildren = item.menuType !== '1';
});
};
const tableList = computed(() => {
const list = state.dataList;
if (Array.isArray(list)) {
setHasChildren(list);
}
return list;
});
// 打开新增菜单弹窗
const onOpenAddMenu = (type?: string, row?: any) => {
menuDialogRef.value.openDialog(type, row);
};
// 打开编辑菜单弹窗
const onOpenEditMenu = (type: string, row: any) => {
menuDialogRef.value.openDialog(type, row);
};
//是否禁用删除
const deleteMenuDisabled = (row: any) => {
return (row.children || []).length > 0;
};
// 搜索事件
const query = () => {
state.dataList = [];
state.queryForm.parentId = undefined;
getDataList();
};
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
state.dataList = [];
getDataList();
};
const load = (row: any, treeNode: unknown, resolve: (date: any[]) => void) => {
const param = {
parentId: row.id,
};
pageList(param).then((res) => {
const childrenList = res.data;
if (Array.isArray(childrenList)) {
setHasChildren(childrenList);
}
resolve(childrenList);
});
};
// 删除操作
const handleDelete = async (row: any) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(row.id);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,129 @@
<template>
<el-dialog :title="form.postId ? $t('common.editBtn') : $t('common.addBtn')" v-model="visible" width="600" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="90px" v-loading="loading">
<el-form-item :label="t('post.postCode')" prop="postCode">
<el-input v-model="form.postCode" :placeholder="t('post.inputpostCodeTip')" />
</el-form-item>
<el-form-item :label="t('post.postName')" prop="postName">
<el-input v-model="form.postName" :placeholder="t('post.inputpostNameTip')" />
</el-form-item>
<el-form-item :label="t('post.postSort')" prop="postSort">
<el-input-number v-model="form.postSort" :placeholder="t('post.inputpostSortTip')" />
</el-form-item>
<el-form-item :label="t('post.remark')" prop="remark">
<el-input type="textarea" maxlength="100" row="3" v-model="form.remark" :placeholder="t('post.inputremarkTip')" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="systemPostDialog">
import { useMessage } from '/@/hooks/message';
import { getObj, addObj, putObj, validatePostCode, validatePostName } from '/@/api/admin/post';
import { useI18n } from 'vue-i18n';
import {rule} from "/@/utils/validate";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 提交表单数据
const form = reactive({
postId: '',
postCode: '',
postName: '',
postSort: 0,
remark: '',
delFlag: '',
createTime: '',
createBy: '',
updateTime: '',
updateBy: '',
});
// 定义校验规则
const dataRules = ref({
postCode: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '岗位编码不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validatePostCode(rule, value, callback, form.postId !== '');
},
trigger: 'blur',
},
],
postName: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '岗位名称不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validatePostName(rule, value, callback, form.postId !== '');
},
trigger: 'blur',
},
],
postSort: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '岗位排序不能为空', trigger: 'blur' }],
remark: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '岗位描述不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.postId = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取Post信息
if (id) {
form.postId = id;
getPostData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
form.postId ? await putObj(form) : await addObj(form);
useMessage().success(t(form.postId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表格数据
const getPostData = (id: string) => {
// 获取部门数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,28 @@
export default {
post: {
index: '#',
importPostTip: ' import Post',
postId: 'postId',
postCode: 'postCode',
postName: 'postName',
postSort: 'postSort',
remark: 'remark',
delFlag: 'delFlag',
createTime: 'createTime',
createBy: 'createBy',
updateTime: 'updateTime',
updateBy: 'updateBy',
tenantId: 'tenantId',
inputpostIdTip: 'input postId',
inputpostCodeTip: 'input postCode',
inputpostNameTip: 'input postName',
inputpostSortTip: 'input postSort',
inputremarkTip: 'input remark',
inputdelFlagTip: 'input delFlag',
inputcreateTimeTip: 'input createTime',
inputcreateByTip: 'input createBy',
inputupdateTimeTip: 'input updateTime',
inputupdateByTip: 'input updateBy',
inputtenantIdTip: 'input tenantId',
},
};

View File

@@ -0,0 +1,28 @@
export default {
post: {
index: '#',
importPostTip: '导入岗位管理',
postId: '岗位ID',
postCode: '岗位编码',
postName: '岗位名称',
postSort: '岗位排序',
remark: '岗位描述',
delFlag: '是否删除 -1已删除 0正常',
createTime: '创建时间',
createBy: '创建人',
updateTime: '更新时间',
updateBy: '更新人',
tenantId: '租户ID',
inputpostIdTip: '请输入岗位ID',
inputpostCodeTip: '请输入岗位编码',
inputpostNameTip: '请输入岗位名称',
inputpostSortTip: '请输入岗位排序',
inputremarkTip: '请输入岗位描述',
inputdelFlagTip: '请输入是否删除 -1已删除 0正常',
inputcreateTimeTip: '请输入创建时间',
inputcreateByTip: '请输入创建人',
inputupdateTimeTip: '请输入更新时间',
inputupdateByTip: '请输入更新人',
inputtenantIdTip: '请输入租户ID',
},
};

View File

@@ -0,0 +1,143 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('post.postName')" prop="postName">
<el-input :placeholder="$t('post.inputpostNameTip')" style="max-width: 180px" v-model="state.queryForm.postName" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="formDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary" v-auth="'sys_post_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain @click="excelUploadRef.show()" class="ml10" icon="upload-filled" type="primary" v-auth="'sys_post_add'">
{{ $t('common.importBtn') }}
</el-button>
<el-button plain :disabled="multiple" @click="handleDelete(selectObjs)" class="ml10" icon="Delete" type="primary" v-auth="'sys_post_del'">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'sys_post_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
style="width: 100%"
v-loading="state.loading"
row-key="postId"
border
:cell-style="tableStyle?.cellStyle"
:header-cell-style="tableStyle?.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="t('post.index')" type="index" width="60" />
<el-table-column :label="t('post.postCode')" prop="postCode" show-overflow-tooltip />
<el-table-column :label="t('post.postName')" prop="postName" show-overflow-tooltip />
<el-table-column :label="t('post.postSort')" prop="postSort" show-overflow-tooltip />
<el-table-column :label="t('post.remark')" prop="remark" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="200">
<template #default="scope">
<el-button icon="edit-pen" @click="formDialogRef.openDialog(scope.row.postId)" text type="primary" v-auth="'sys_post_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" @click="handleDelete([scope.row.postId])" text type="primary" v-auth="'sys_post_del'"
>{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog @refresh="getDataList()" ref="formDialogRef" />
<!-- 导入excel -->
<upload-excel
:title="$t('post.importPostTip')"
@refreshDataList="getDataList"
ref="excelUploadRef"
temp-url="/admin/sys-file/local/file/post.xlsx"
url="/admin/post/import"
/>
</div>
</template>
<script lang="ts" name="systemPost" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchList } from '/@/api/admin/post';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义变量内容
const formDialogRef = ref();
const excelUploadRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/post/export', Object.assign(state.queryForm,{ids:selectObjs}), 'post.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { postId: string }[]) => {
selectObjs.value = objs.map(({ postId }) => postId);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,203 @@
<template>
<el-dialog :close-on-click-modal="false" :title="form.roleId ? $t('common.editBtn') : $t('common.addBtn')" width="600" draggable v-model="visible">
<el-form :model="form" :rules="dataRules" label-width="90px" ref="dataFormRef" v-loading="loading">
<el-form-item :label="$t('sysrole.roleName')" prop="roleName">
<el-input :placeholder="$t('sysrole.please_enter_a_role_name')" clearable v-model="form.roleName"></el-input>
</el-form-item>
<el-form-item :label="$t('sysrole.roleCode')" prop="roleCode">
<el-input :placeholder="$t('sysrole.please_enter_the_role_Code')" :disabled="form.roleId !== ''" clearable v-model="form.roleCode"></el-input>
</el-form-item>
<el-form-item :label="$t('sysrole.roleDesc')" prop="roleDesc">
<el-input
maxlength="100"
:rows="3"
:placeholder="$t('sysrole.please_enter_the_role_description')"
type="textarea"
v-model="form.roleDesc"
></el-input>
</el-form-item>
<el-form-item :label="$t('sysrole.menu_authority')" prop="dsType">
<el-select :placeholder="$t('sysrole.please_select')" clearable v-model="form.dsType">
<el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in dictType" />
</el-select>
</el-form-item>
<el-form-item v-if="form.dsType === 1">
<el-tree
:check-strictly="true"
:data="dataForm.deptData"
:default-checked-keys="dataForm.checkedDsScope"
:props="dataForm.deptProps"
default-expand-all
highlight-current
node-key="id"
ref="deptTreeRef"
show-checkbox
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="systemRoleDialog" setup>
import { rule } from '/@/utils/validate';
import { deptTree } from '/@/api/admin/dept';
import { useMessage } from '/@/hooks/message';
import { addObj, getObj, putObj, validateRoleCode, validateRoleName } from '/@/api/admin/role';
import { useI18n } from 'vue-i18n';
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const deptTreeRef = ref();
const visible = ref(false);
const loading = ref(false);
// 提交表单数据
const form = reactive({
roleId: '',
roleName: '',
roleCode: '',
roleDesc: '',
dsType: 0,
dsScope: '',
});
// 页面对应元数据
const dataForm = reactive({
deptData: [],
checkedDsScope: [],
deptProps: {
children: 'children',
label: 'name',
value: 'id',
},
});
// 定义校验规则
const dataRules = ref({
roleName: [
{ required: true, message: '角色名称不能为空', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateRoleName(rule, value, callback, form.roleId !== '');
},
trigger: 'blur',
},
],
roleCode: [
{ required: true, message: '角色标识不能为空', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
{ validator: rule.validatorCapital, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateRoleCode(rule, value, callback, form.roleId !== '');
},
trigger: 'blur',
},
],
roleDesc: [{ validator: rule.overLength, trigger: 'blur' }],
dsType: [{ required: true, message: '请选择数据权限类型', trigger: 'blur' }],
menu_authority: [{ required: true, message: '数据权限不能为空', trigger: 'blur' }],
});
const dictType = ref([
{
label: '全部',
value: 0,
},
{
label: '自定义',
value: 1,
},
{
label: '本级及子级',
value: 2,
},
{
label: '本级',
value: 3,
},
{
label: '本人',
value: 4,
},
]);
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.roleId = '';
nextTick(() => {
dataFormRef.value.resetFields();
});
// 获取角色信息
if (id) {
form.roleId = id;
getRoleData(id);
}
getDeptData();
};
// 提交
const onSubmit = async () => {
if (form.dsType === 1) {
form.dsScope = deptTreeRef.value.getCheckedKeys().join(',');
} else {
form.dsScope = '';
}
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
form.roleId ? await putObj(form) : await addObj(form);
useMessage().success(t(form.roleId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化角色数据
const getRoleData = (id: string) => {
// 获取部门数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
if (res.data.dsScope) {
dataForm.checkedDsScope = res.data.dsScope.split(',');
} else {
dataForm.checkedDsScope = [];
}
});
};
// 获取菜单结构数据
const getDeptData = () => {
deptTree().then((res: any) => {
dataForm.deptData = res.data;
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,20 @@
export default {
sysrole: {
index: '#',
roleName: 'roleName',
inputRoleNameTip: 'input roleName',
permissionTip: 'grant',
deleteDisabledTip: 'not allowed to delete',
mustCheckOneTip: 'the assign permissions menu must be selected',
roleCode: 'roleCode',
roleDesc: 'role description',
data_authority: 'data authority',
createTime: 'createTime',
please_enter_a_role_name: 'please enter a role name',
please_enter_the_role_Code: 'please enter the role Code',
please_enter_the_role_description: 'please enter the role description',
menu_authority: 'menu authority',
please_select: 'please select',
importRoleTip: 'import role',
},
};

View File

@@ -0,0 +1,20 @@
export default {
sysrole: {
index: '#',
roleName: '角色名称',
inputRoleNameTip: '请输入角色名称',
permissionTip: '授权',
deleteDisabledTip: '角色不允许删除',
mustCheckOneTip: '必须选择【分配权限】菜单',
roleCode: '角色标识',
roleDesc: '角色描述',
data_authority: '数据权限',
createTime: '创建时间',
please_enter_a_role_name: '请输入角色名称',
please_enter_the_role_Code: '请输入角色标识',
please_enter_the_role_description: '请输入角色描述',
menu_authority: '数据权限',
please_select: '请选择',
importRoleTip: '导入角色'
},
};

View File

@@ -0,0 +1,197 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item :label="$t('sysrole.roleName')" prop="roleName">
<el-input :placeholder="$t('sysrole.inputRoleNameTip')" v-model="state.queryForm.roleName" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="roleDialogRef.openDialog()" v-auth="'sys_role_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain icon="upload-filled" type="primary" class="ml10" @click="excelUploadRef.show()" v-auth="'sys_user_add'">
{{ $t('common.importBtn') }}
</el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary" class="ml10" v-auth="'sys_user_del'" @click="handleDelete(selectObjs)">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_role_export'"
@exportExcel="exportExcel"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
style="width: 100%"
row-key="roleId"
@selection-change="handleSelectionChange"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" :selectable="handleSelectable" width="50" align="center" />
<el-table-column type="index" :label="$t('sysrole.index')" width="80" />
<el-table-column prop="roleName" :label="$t('sysrole.roleName')" show-overflow-tooltip></el-table-column>
<el-table-column prop="roleCode" :label="$t('sysrole.roleCode')" show-overflow-tooltip></el-table-column>
<el-table-column prop="roleDesc" :label="$t('sysrole.roleDesc')" show-overflow-tooltip></el-table-column>
<el-table-column prop="data_authority" :label="$t('sysrole.data_authority')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dictType" :value="scope.row.dsType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" :label="$t('sysrole.createTime')" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" width="250">
<template #default="scope">
<el-button text type="primary" icon="edit-pen" v-auth="'sys_role_edit'" @click="roleDialogRef.openDialog(scope.row.roleId)">{{
$t('common.editBtn')
}}</el-button>
<el-button text type="primary" icon="turn-off" v-auth="'sys_role_perm'" @click="permessionRef.openDialog(scope.row)">{{
$t('sysrole.permissionTip')
}}</el-button>
<el-tooltip :content="$t('sysrole.deleteDisabledTip')" :disabled="scope.row.roleId !== '1'" placement="top">
<span style="margin-left: 12px">
<el-button
text
type="primary"
icon="delete"
:disabled="scope.row.roleId === '1'"
v-auth="'sys_role_del'"
@click="handleDelete([scope.row.roleId])"
>{{ $t('common.delBtn') }}
</el-button>
</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<!-- 角色编辑新增 -->
<role-dialog ref="roleDialogRef" @refresh="getDataList()" />
<!-- 导入角色 -->
<upload-excel
ref="excelUploadRef"
:title="$t('sysrole.importRoleTip')"
url="/admin/role/import"
temp-url="/admin/sys-file/local/file/role.xlsx"
@refreshDataList="getDataList"
/>
<!-- 授权 -->
<permession ref="permessionRef" />
</div>
</template>
<script setup lang="ts" name="systemRole">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { pageList, delObj } from '/@/api/admin/role';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 引入组件
const RoleDialog = defineAsyncComponent(() => import('./form.vue'));
const Permession = defineAsyncComponent(() => import('./permession.vue'));
const { t } = useI18n();
// 定义变量内容
const roleDialogRef = ref();
const permessionRef = ref();
const excelUploadRef = ref();
const queryRef = ref();
const showSearch = ref(true);
// 多选rows
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
roleName: '',
},
pageList: pageList, // H
descs: ['create_time'],
});
const dictType = ref([
{
label: '全部',
value: '0',
},
{
label: '自定义',
value: '1',
},
{
label: '本级及子级',
value: '2',
},
{
label: '本级',
value: '3',
},
{
label: '本人',
value: '4',
},
]);
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/role/export',Object.assign(state.queryForm,{ids:selectObjs}), 'role.xlsx');
};
// 是否可以多选
const handleSelectable = (row: any) => {
return row.roleId !== '1';
};
// 多选事件
const handleSelectionChange = (objs: { roleId: string }[]) => {
selectObjs.value = objs.map(({ roleId }) => roleId);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,137 @@
<template>
<div class="system-role-dialog-container">
<el-dialog width="30%" v-model="state.dialog.isShowDialog" :close-on-click-modal="false" draggable>
<template #header>
<div class="flex items-center justify-between">
<div>{{ state.dialog.title }}</div>
<div class="flex mr-16">
<el-checkbox :label="$t('common.expand')" @change="handleExpand"/>
<el-checkbox :label="$t('common.selectAll')" @change="handleSelectAll"/>
</div>
</div>
</template>
<el-scrollbar class="h-[400px] sm:h-[600px]">
<el-tree
v-loading="loading"
ref="menuTree"
:data="state.treeData"
:default-checked-keys="state.checkedKeys"
:check-strictly="!checkStrictly"
:props="state.defaultProps"
class="filter-tree"
node-key="id"
highlight-current
show-checkbox
/>
</el-scrollbar>
<template #footer>
<span class="dialog-footer">
<el-button @click="state.dialog.isShowDialog = false"> </el-button>
<el-button type="primary" @click="onSubmit">{{ state.dialog.submitTxt }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="role-permession">
import {fetchRoleTree, permissionUpd} from '/@/api/admin/role';
import {pageList} from '/@/api/admin/menu';
import {useMessage} from '/@/hooks/message';
import {Ref} from 'vue';
import {useI18n} from 'vue-i18n';
import other from '/@/utils/other';
import {CheckboxValueType} from 'element-plus';
const {t} = useI18n();
const menuTree = ref();
const checkStrictly = ref(true);
const loading = ref(false);
const state = reactive({
checkedKeys: [] as any[],
treeData: [] as any[],
defaultProps: {
label: 'name',
value: 'id',
},
roleId: '',
dialog: {
isShowDialog: false,
title: '分配权限',
submitTxt: '更新',
},
});
const checkedKeys: Ref<any[]> = ref([]);
// 打开弹窗
const openDialog = (row: any) => {
state.checkedKeys = [];
state.treeData = [];
checkedKeys.value = [];
state.roleId = row.roleId;
loading.value = true;
fetchRoleTree(row.roleId)
.then((res) => {
checkedKeys.value = res.data;
return pageList();
})
.then((r) => {
state.treeData = r.data;
state.checkedKeys = other.resolveAllEunuchNodeId(state.treeData, checkedKeys.value, []);
})
.finally(() => {
loading.value = false;
});
state.dialog.isShowDialog = true;
};
const handleExpand = (check: CheckboxValueType) => {
const treeList = state.treeData;
for (let i = 0; i < treeList.length; i++) {
//@ts-ignore
menuTree.value.store.nodesMap[treeList[i].id].expanded = check;
}
};
const handleSelectAll = (check: CheckboxValueType) => {
if (check) {
menuTree.value?.setCheckedKeys(state.treeData.map((item) => item.id));
} else {
menuTree.value?.setCheckedKeys([]);
}
};
// 提交授权数据
const onSubmit = () => {
// 初始角色选择节点必须包含 【分配权限】 菜单
if (state.roleId === '1') {
if (!menuTree.value.getCheckedNodes().map((item: { name: string }) => {
return item.name;
}).includes('分配权限')) {
useMessage().error(t('sysrole.mustCheckOneTip'));
return;
}
}
const menuIds = menuTree.value.getCheckedKeys().join(',').concat(',').concat(menuTree.value.getHalfCheckedKeys().join(','));
loading.value = true;
permissionUpd(state.roleId, menuIds)
.then(() => {
state.dialog.isShowDialog = false;
useMessage().success(t('common.editSuccessText'));
})
.finally(() => {
loading.value = false;
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>
<style scoped></style>

View File

@@ -0,0 +1,244 @@
<template>
<el-drawer :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" v-model="visible" :close-on-click-modal="true" draggable size="50%">
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="90px" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="t('tenant.name')" prop="name">
<el-input v-model="form.name" :placeholder="t('tenant.inputnameTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tenant.code')" prop="code">
<el-input v-model="form.code" :placeholder="t('tenant.inputcodeTip')" :disabled="form.id !== ''" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tenant.startTime')" prop="startTime">
<el-date-picker class="!w-full" v-model="form.startTime" type="date" :placeholder="t('tenant.inputstartTimeTip')" :value-format="dateTimeStr" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tenant.endTime')" prop="endTime">
<el-date-picker class="!w-full" v-model="form.endTime" type="date" :placeholder="t('tenant.inputendTimeTip')" :value-format="dateTimeStr" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :span="12" :label="t('tenant.tenantDomain')" prop="tenantDomain">
<el-input v-model="form.tenantDomain" :placeholder="t('tenant.inputtenantDomainTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('tenant.status')" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="item.value" v-for="(item, index) in status_type" border :key="index">{{ item.label }} </el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-divider content-position="left" v-if="form.id !== '1'">
<div>
<span class="mr-4">{{ $t('tenantmenu.name') }}</span>
<el-checkbox :label="$t('common.expand')" @change="handleExpand" />
<el-checkbox :label="$t('common.selectAll')" @change="handleSelectAll" />
</div>
</el-divider>
<el-scrollbar class="h-[400px] sm:h-[600px] ml-12" v-if="form.id !== '1'">
<el-tree
show-checkbox
ref="menuTreeRef"
:disabled="true"
:check-strictly="false"
:data="menuData"
:props="defaultProps"
:default-checked-keys="checkedMenu"
node-key="id"
highlight-current
/>
</el-scrollbar>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-drawer>
</template>
<script setup lang="ts" name="systemTenantDialog">
import { validateTenantCode, validateTenantName } from '/@/api/admin/tenant';
import { useDict } from '/@/hooks/dict';
import { useMessage } from '/@/hooks/message';
import { getObj, addObj, putObj, treemenu,fetchList } from '/@/api/admin/tenant';
import { useI18n } from 'vue-i18n';
import other from '/@/utils/other';
import { CheckboxValueType } from 'element-plus';
import {rule} from "/@/utils/validate";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const menuTreeRef = ref();
const visible = ref(false);
const loading = ref(false);
// 字典
const { status_type } = useDict('status_type');
// 提交表单数据
const form = reactive({
id: '',
name: '',
code: '',
tenantDomain: '',
startTime: '',
endTime: '',
status: '0',
delFlag: '',
createBy: '',
updateBy: '',
createTime: '',
updateTime: '',
menuId: '',
});
const menuData = ref<any[]>([]);
const defaultProps = reactive({
label: 'name',
value: 'id',
disabled: true,
});
const checkedMenu = ref<any[]>([]);
// 定义校验规则
const dataRules = ref({
name: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '名称不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateTenantName(rule, value, callback, form.id !== '');
},
trigger: 'blur',
},
],
code: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '编码不能为空', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateTenantCode(rule, value, callback, form.id !== '');
},
trigger: 'blur',
},
],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }],
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'blur' }],
status: [{ required: true, message: 'status不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string): void => {
visible.value = true;
form.id = '';
form.menuId = '';
checkedMenu.value = [];
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
if (id) {
form.id = id;
getTenantData(id);
}
getMenuData();
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
if (menuTreeRef.value?.getCheckedKeys().length === 0) {
useMessage().error('请选择租户套餐菜单');
return false;
}
if (menuTreeRef.value?.getCheckedKeys()) {
let checkMenu = [...menuTreeRef.value.getCheckedKeys(), ...menuTreeRef.value.getHalfCheckedKeys()]
if (!checkMenu.includes('1300')) {
useMessage().error('必须分配角色管理功能');
return false;
}
if (!checkMenu.includes('1302')) {
useMessage().error('必须分配角色管理功能');
return false;
}
form.menuId = checkMenu.join(',');
}
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
useMessage().success(t(form.id ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
await fetchList()
loading.value = false;
}
};
/**
* 初始化表格数据。
* @param {string} id - 部门 ID。
*/
const getTenantData = async (id) => {
const res = await getObj(id);
Object.assign(form, res.data);
};
/**
* 获取菜单数据
*/
const getMenuData = async () => {
const res = await treemenu();
menuData.value = res.data;
checkedMenu.value = form.menuId ? other.resolveAllEunuchNodeId(menuData.value, form.menuId.split(','), []) : [];
};
const handleExpand = (check: CheckboxValueType) => {
const treeList = menuData.value;
for (let i = 0; i < treeList.length; i++) {
//@ts-ignore
menuTreeRef.value.store.nodesMap[treeList[i].id].expanded = check;
}
};
const handleSelectAll = (check: CheckboxValueType) => {
if (check) {
menuTreeRef.value?.setCheckedKeys(menuData.value.map((item) => item.id));
} else {
menuTreeRef.value?.setCheckedKeys([]);
}
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,40 @@
export default {
tenant: {
index: '#',
importTenantTip: ' import Tenant',
id: 'id',
name: 'name',
code: 'code',
tenantDomain: 'tenantDomain',
startTime: 'startTime',
endTime: 'endTime',
status: 'status',
delFlag: 'delFlag',
createBy: 'createBy',
updateBy: 'updateBy',
createTime: 'createTime',
updateTime: 'updateTime',
menuId: 'menuIds',
individuationBtn: 'individuation',
inputidTip: 'input id',
inputnameTip: 'input name',
inputcodeTip: 'input code',
inputtenantDomainTip: 'input tenantDomain',
inputstartTimeTip: 'input startTime',
inputendTimeTip: 'input endTime',
inputstatusTip: 'input status',
inputdelFlagTip: 'input delFlag',
inputcreateByTip: 'input createBy',
inputupdateByTip: 'input updateBy',
inputcreateTimeTip: 'input createTime',
inputupdateTimeTip: 'input updateTime',
inputmenuIdTip: 'input menuId',
deleteDisabledTip: 'base tenants are not allowed to delete',
},
tenantmenu: {
name: 'tenantmenu',
index: '#',
status: 'status',
createTime: 'createTime',
},
};

View File

@@ -0,0 +1,53 @@
export default {
tenant: {
index: '#',
importTenantTip: '导入租户',
id: '租户id',
name: '租户名称',
code: '编码',
tenantDomain: '域名',
startTime: '开始时间',
endTime: '结束时间',
status: '状态',
delFlag: 'delFlag',
createBy: '创建人',
updateBy: '修改人',
createTime: '创建',
updateTime: '更新时间',
menuId: '租户套餐',
individuationBtn: '个性化',
inputidTip: '请输入租户id',
inputnameTip: '请输入名称',
inputcodeTip: '请输入编码',
inputtenantDomainTip: '请输入域名',
inputstartTimeTip: '请输入开始时间',
inputendTimeTip: '请输入结束时间',
inputstatusTip: '请输入status',
inputdelFlagTip: '请输入delFlag',
inputcreateByTip: '请输入创建人',
inputupdateByTip: '请输入修改人',
inputcreateTimeTip: '请输入创建',
inputupdateTimeTip: '请输入更新时间',
inputmenuIdTip: '请选择租户套餐',
deleteDisabledTip: '基础租户不允许删除',
},
tenantmenu: {
name: '套餐',
index: '#',
status: '状态',
createTime: '创建',
},
individuation: {
websiteName: '网站名称',
miniQr: '移动端二维码',
logo: '网站图标',
footerAuthor: '页脚信息',
background: '登录页背景图',
inputIndividuationNameTip: '请输入网站名称',
inputMiniQrTip: '请输入网站图标',
inputLogoTip: '请输入网站Logo',
inputFooterAuthorTip: '请输入页脚信息',
inputBackgroundTip: '请输入登录页背景图',
}
};

View File

@@ -0,0 +1,240 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('tenant.name')" prop="name">
<el-input :placeholder="$t('tenant.inputnameTip')" style="max-width: 180px" v-model="state.queryForm.name" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="formDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary" v-auth="'sys_systenant_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain @click="handleRefreshCache()" class="ml10" icon="refresh-left" type="primary">
{{ $t('common.refreshCacheBtn') }}
</el-button>
<el-button
plain
:disabled="multiple"
@click="handleDelete(selectObjs)"
class="ml10"
icon="Delete"
type="primary"
v-auth="'sys_systenant_del'"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'sys_systenant_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-scrollbar>
<div class="mx-auto mt-4">
<div class="px-4">
<div class="grid sm:grid-cols-2 sm:gap-x-6 lg:grid-cols-3">
<div class="p-6 mb-6 bg-gray-100 rounded-lg dark:bg-gray-800" v-for="tenant in state.dataList" :key="tenant.id">
<div class="flex items-center justify-between">
<div class="flex items-center">
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">{{ tenant.name }}</h3>
</div>
<div class="flex items-end">
<svg
t="1710908184286"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5178"
class="w-5 h-5 mr-2 text-base text-gray-500 dark:text-gray-400"
>
<path
d="M512 1003.52a497.92 497.92 0 1 1 497.92-497.92A498.56 498.56 0 0 1 512 1003.52zM512 71.68a433.92 433.92 0 1 0 433.92 433.92A434.56 434.56 0 0 0 512 71.68z"
fill="#323333"
p-id="5179"
></path>
<path
d="M152.96 369.92a33.92 33.92 0 0 1 35.2-36.48 39.04 39.04 0 0 1 29.44 16l148.48 198.4V369.92a35.2 35.2 0 1 1 69.76 0V640a30.72 30.72 0 0 1-34.56 33.28 36.48 36.48 0 0 1-29.44-12.8l-147.2-198.4V640a30.72 30.72 0 0 1-34.56 33.28 31.36 31.36 0 0 1-37.12-33.28zM463.36 504.32a162.56 162.56 0 1 1 323.84 0 158.08 158.08 0 0 1-161.92 168.96 159.36 159.36 0 0 1-161.92-168.96z m252.16 0c-3.84-69.12-33.92-104.96-90.24-108.8s-84.48 39.68-88.32 108.8 33.28 104.32 88.32 108.16 86.4-36.48 90.24-108.16zM856.96 603.52A37.12 37.12 0 0 1 896 640c0 21.12-13.44 32-36.48 33.28s-35.84-12.16-37.12-33.28a37.76 37.76 0 0 1 34.56-36.48z"
fill="#323333"
p-id="5180"
></path>
</svg>
<span class="mr-1 dark:text-gray-300">{{ tenant.code }}</span>
</div>
</div>
<p class="my-3 text-sm font-normal text-gray-500 dark:text-gray-400">
状态 {{ status_type.find((item: { value: string }) => item.value === tenant.status)?.label }}
</p>
<p class="my-3 text-sm font-normal text-gray-500 dark:text-gray-400">
有效期 {{ parseDate(tenant.startTime) }} - {{ parseDate(tenant.endTime) }}
</p>
<div class="flex items-center justify-between mt-6 text-sm font-semibold text-gray-900 dark:text-gray-100">
<div class="flex">
<svg
t="1710908265535"
class="w-6 h-5 mr-1 text-yellow-500"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="6175"
width="128"
height="128"
>
<path
d="M512.001274 15.045039a497.880869 497.880869 0 0 1 496.99699 497.086142c0 137.285182-55.886849 261.573274-145.67566 351.652466-89.81301 90.098296-214.335444 145.167493-351.32133 145.167494-137.262257 0-261.529972-55.069197-351.31751-145.173862-89.788811-90.079192-145.67566-214.367284-145.67566-351.646098a497.880869 497.880869 0 0 1 496.99317-497.086142zM489.215293 932.144136V771.518957a405.118201 405.118201 0 0 0-129.123952 26.595319 137.138718 137.138718 0 0 0-17.101903 6.511917c3.53042 7.062113 6.785742 14.098754 10.570881 20.887043 25.514032 45.602527 55.33538 80.88762 88.987717 101.223193a372.744559 372.744559 0 0 0 46.671078 5.40898z m320.120674-717.103117a128.736778 128.736778 0 0 0-10.591259-10.061441 441.222218 441.222218 0 0 1-58.315604 36.934404c27.141693 70.795612 43.939205 156.01602 46.672351 247.447775h144.843999a419.629601 419.629601 0 0 0-122.609487-274.320738z m-46.672352-40.41388a415.300634 415.300634 0 0 0-85.71329-49.415687 434.384259 434.384259 0 0 1 33.623044 51.562976c3.805517 7.062113 7.861934 15.201692 11.942549 23.339997 13.571483-8.138305 27.117495-16.556802 40.137509-25.493654z m-181.233302-77.3572c-15.172399-2.404557-30.921738-4.056417-46.64688-5.133883V253.306336a429.022402 429.022402 0 0 0 129.127773-26.588951c5.985921-2.177856 11.392353-4.356986 17.076431-6.787015-2.954752-7.337211-7.036641-14.373851-10.541588-20.887043-24.989309-46.15527-55.610477-80.888893-89.013189-101.774662z m-92.217567-5.133883c-15.723868 1.077466-31.473207 2.729325-46.671078 5.133883-33.652337 20.887043-63.473685 55.619393-88.987717 101.774662-3.781319 6.510644-7.036641 13.547285-10.816686 20.887042 5.684078 2.430029 11.392353 4.609159 17.351529 6.787016a428.859382 428.859382 0 0 0 129.123952 26.595318V92.130962z m-142.141419 33.075396a414.845959 414.845959 0 0 0-85.732395 49.415687c13.020014 8.940673 26.591498 17.355349 40.412607 25.493654a233.26369 233.26369 0 0 1 11.667451-23.339997c10.041064-19.007209 21.708515-35.811089 33.652337-51.562976z m-122.088585 79.763031c-3.255322 3.004423-7.060839 6.536116-10.316162 10.061441a419.628328 419.628328 0 0 0-122.614581 274.32456h144.874565c2.703853-91.430482 19.529385-176.648342 46.370508-247.447775a478.47757 478.47757 0 0 1-58.31433-36.934405zM92.051999 535.189712a420.779662 420.779662 0 0 0 122.614581 274.319464l10.316162 10.293237a425.318773 425.318773 0 0 1 58.318151-37.413278c-26.841123-71.096182-43.666655-155.742195-46.370508-247.19815H92.051999z m169.286933 314.488813a415.88267 415.88267 0 0 0 85.732395 49.383847 360.53328 360.53328 0 0 1-33.652337-51.562976c-4.075521-7.313012-8.137031-15.176219-11.667451-22.788528a416.635367 416.635367 0 0 0-40.412607 24.962563z m273.441954 82.465611a371.780443 371.780443 0 0 0 46.646879-5.40898c33.402711-20.335574 64.02388-55.620666 89.013189-101.223194 3.504948-6.787015 7.586836-13.823656 10.541589-20.887042a133.885943 133.885943 0 0 0-17.076431-6.511918 405.259571 405.259571 0 0 0-129.120132-26.594045v160.625179z m142.169439-33.081764a416.339892 416.339892 0 0 0 85.71329-49.383847 390.678122 390.678122 0 0 0-40.137509-24.962563c-4.075521 7.612308-8.137031 15.475516-11.942548 22.788528-10.592533 18.706639-21.684316 36.362558-33.623045 51.562976z m121.789288-79.259959l10.591259-10.293237a420.780936 420.780936 0 0 0 122.614582-274.319464H787.101455c-2.729325 91.455954-19.530658 176.101968-46.672351 247.198149a395.620965 395.620965 0 0 1 58.315604 37.414552z m-99.82478-558.105599a203.624467 203.624467 0 0 1-19.004662 7.33721 460.617875 460.617875 0 0 1-145.124191 29.826442v190.500018h206.195866c-2.452954-84.393841-17.375727-162.553409-42.064466-227.66367z m-209.69954 37.163652a456.062207 456.062207 0 0 1-144.873292-29.826442 187.929892 187.929892 0 0 1-19.530658-7.33721c-24.412368 65.110261-39.608965 143.269829-42.064466 227.66367H489.215293V298.860466z m0 426.554165v-190.224919H282.746877c2.454228 84.144215 17.652098 161.977742 42.064466 227.388572 6.510644-2.428756 13.020014-5.158081 19.530658-7.33721 45.019218-17.354076 93.864332-27.396413 144.873292-29.826443z m45.570687 0a465.365857 465.365857 0 0 1 144.874566 29.826443c6.50937 2.17913 12.743643 4.908455 19.254287 7.33721 24.688739-65.410831 39.608965-143.244357 42.064466-227.388572H534.78598v190.224919z"
fill="#3B3F51"
p-id="6176"
></path>
</svg>
<span class="dark:text-gray-300">{{ tenant.tenantDomain }}</span>
</div>
<div class="flex items-center">
<el-button
class="!p-0"
icon="HomeFilled"
@click="individuationRef.openDialog(tenant.id)"
text
type="primary"
v-auth="'sys_systenant_edit'"
>{{ $t('tenant.individuationBtn') }}
</el-button>
<el-button
class="!p-0"
icon="edit-pen"
@click="formDialogRef.openDialog(tenant.id)"
text
type="primary"
v-auth="'sys_systenant_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-button
class="!p-0"
icon="delete"
:disabled="tenant.id === '1'"
@click="handleDelete([tenant.id])"
text
type="primary"
v-auth="'sys_systenant_del'"
>{{ $t('common.delBtn') }}
</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog @refresh="getDataList()" ref="formDialogRef" />
<!-- 编辑新增 -->
<individuation ref="individuationRef" />
<!-- 导入excel -->
<upload-excel
:title="$t('tenant.importTenantTip')"
@refreshDataList="getDataList"
ref="excelUploadRef"
temp-url="/admin/sys-file/local/file/tenant.xlsx"
url="/admin/tenant/import"
/>
</div>
</template>
<script lang="ts" name="systemTenant" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchPage, fetchList } from '/@/api/admin/tenant';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
import { useDict } from '/@/hooks/dict';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const Individuation = defineAsyncComponent(() => import('./individuation.vue'));
const { t } = useI18n();
// 定义变量内容
const formDialogRef = ref();
const individuationRef = ref();
const excelUploadRef = ref();
const tenantMenuRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
// 字典
const { status_type } = useDict('status_type');
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchPage,
pagination: {
size: 6,
pageSizes: [3, 6, 9, 12],
},
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/tenant/export', Object.assign(state.queryForm, { ids: selectObjs }), 'tenant.xlsx');
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
} finally {
handleRefreshCache();
}
};
//刷新缓存
const handleRefreshCache = () => {
fetchList().then(() => {
useMessage().success('同步成功');
});
};
</script>

View File

@@ -0,0 +1,137 @@
<template>
<el-drawer :title="$t('tenant.individuationBtn')" v-model="visible" :close-on-click-modal="false">
<el-form ref="dataFormRef" :model="form" :rules="dataRules" v-loading="loading">
<el-row>
<el-col :span="24" class="mt-4">
<el-form-item :label="t('individuation.websiteName')" prop="websiteName" label-width="120px" align="left">
<el-input v-model="form.websiteName" :placeholder="t('individuation.inputIndividuationNameTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mt-4">
<el-form-item prop="footerAuthor" label-width="120px" align="left">
<template #label>
{{ t('individuation.footerAuthor') }}
<tip content="浏览器底部版权信息、备案信息"/>
</template>
<el-input v-model="form.footer" :placeholder="t('individuation.inputFooterAuthorTip')"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mt-4">
<el-form-item prop="icon" label-width="120px" align="left">
<template #label>
{{ t('individuation.miniQr') }}
<tip content="登录页右下角显示的移动端二维码"/>
</template>
<upload-img v-model:image-url="form.miniQr"/>
</el-form-item>
</el-col>
<el-col :span="24" class="mt-4">
<el-form-item :label="t('individuation.background')" prop="background" label-width="120px" align="left">
<upload-img v-model:image-url="form.background"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-drawer>
</template>
<script setup lang="ts" name="systemTenantDialog">
import {useDict} from '/@/hooks/dict';
import {useMessage} from '/@/hooks/message';
import {getObj, putObj} from '/@/api/admin/tenant';
import {useI18n} from 'vue-i18n';
import UploadImg from "/@/components/Upload/Image.vue";
import {useThemeConfig} from "/@/stores/themeConfig";
import pinia from "/@/stores";
import {storeToRefs} from "pinia";
import Tip from "/@/components/Tip/index.vue";
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const {t} = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 字典
const {status_type} = useDict('status_type');
// 导入配置文件
const stores = useThemeConfig(pinia);
const {themeConfig} = storeToRefs(stores);
// 提交表单数据
const form = reactive({
id: '',
websiteName: themeConfig.value.globalTitle,
background: '',
miniQr: '',
footer: themeConfig.value.footerAuthor,
});
// 定义校验规则
const dataRules = ref({
});
// 打开弹窗
const openDialog = (id: string): void => {
visible.value = true;
form.id = ''
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
if (id) {
form.id = id;
getTenantData(id);
}
};
/**
* 初始化表格数据。
* @param {string} id - 部门 ID。
*/
const getTenantData = async (id: any) => {
const res = await getObj(id);
Object.assign(form, res.data);
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {
});
if (!valid) return false;
try {
loading.value = true;
await putObj(form);
useMessage().success(t('common.editSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,224 @@
<template>
<div class="system-user-dialog-container">
<el-dialog :close-on-click-modal="false" :title="dataForm.userId ? $t('common.editBtn') : $t('common.addBtn')" draggable v-model="visible">
<el-form :model="dataForm" :rules="dataRules" label-width="90px" ref="dataFormRef" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.username')" prop="username">
<el-input :disabled="true" placeholder="请输入用户名" v-model="dataForm.username"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.name')" prop="name">
<el-input disabled clearable placeholder="请输入姓名" v-model="dataForm.name"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="部门/车队" prop="deptName">
<el-input disabled clearable placeholder="请输入手机号" v-model="dataForm.deptName"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('sysuser.role')" prop="role">
<el-select clearable placeholder="请选择角色" v-model="dataForm.roleId">
<el-option :key="item.roleId" :label="item.roleName" :value="item.roleId" v-for="item in roleData" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" name="systemUserDialog" setup>
import { addObj, getObj, putObj, validatePhone, validateUsername } from '/@/api/admin/user';
import { list as roleList } from '/@/api/admin/role';
import { useI18n } from 'vue-i18n';
import { useMessage } from '/@/hooks/message';
import { rule } from '/@/utils/validate';
const { t } = useI18n();
// 定义刷新表格emit
const emit = defineEmits(['refresh']);
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const roleData = ref<any[]>([]);
const loading = ref(false);
const dataForm = reactive({
userId: '',
username: '',
password: '' as String | undefined,
salt: '',
wxOpenid: '',
qqOpenid: '',
lockFlag: '0',
phone: '' as String | undefined,
deptId: '',
roleList: [],
postList: [],
nickname: '',
name: '',
email: '',
post: [] as string[],
role: [] as string[],
roleId: '',
});
const dataRules = ref({
// 用户名校验,不能为空 、长度 5-20、不能和已有数据重复
username: [
{ required: true, message: '用户名不能为空', trigger: 'blur' },
{ min: 5, max: 20, message: '用户名称长度必须介于 5 和 20 之间', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateUsername(rule, value, callback, dataForm.userId !== '');
},
trigger: 'blur',
},
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' },
{
min: 6,
max: 20,
message: '用户密码长度必须介于 5 和 20 之间',
trigger: 'blur',
},
],
// 姓名校验,不能为空、只能是中文
name: [
{ validator: rule.overLength, trigger: 'blur' },
{ required: true, message: '姓名不能为空', trigger: 'blur' },
{ validator: rule.chinese, trigger: 'blur' },
],
deptId: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
role: [{ required: true, message: '角色不能为空', trigger: 'blur' }],
post: [{ required: true, message: '岗位不能为空', trigger: 'blur' }],
// 手机号校验,不能为空、新增的时不能重复校验
phone: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{ validator: rule.validatePhone, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validatePhone(rule, value, callback, dataForm.userId !== '');
},
trigger: 'blur',
},
],
email: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] },
{ validator: rule.overLength, trigger: 'blur' },
],
lockFlag: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = async (id: string) => {
visible.value = true;
dataForm.userId = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 修改获取用户信息
if (id) {
dataForm.userId = id;
await getUserData(id);
dataForm.password = '******';
}
// 加载使用的数据
getRoleData();
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
const { userId, phone, password, roleId } = dataForm;
if (userId) {
// 清除占位符,避免提交错误的数据
if (phone?.includes('*')) dataForm.phone = undefined;
if (password?.includes('******')) dataForm.password = undefined;
loading.value = true;
if (roleId) {
dataForm.role = [roleId];
}
await putObj(dataForm);
useMessage().success(t('common.editSuccessText'));
visible.value = false; // 关闭弹窗
emit('refresh');
} else {
loading.value = true;
await addObj(dataForm);
useMessage().success(t('common.addSuccessText'));
visible.value = false; // 关闭弹窗
emit('refresh');
}
} catch (error: any) {
useMessage().error(error.msg);
} finally {
loading.value = false;
}
};
/**
* 从服务器获取用户数据
*
* @async
* @param {string} id - 用户 ID
* @return {Promise} - 包含用户数据的 Promise 对象
*/
const getUserData = async (id: string): Promise<any> => {
try {
loading.value = true;
const { data } = await getObj(id);
Object.assign(dataForm, data);
if (data.roleList) {
dataForm.role = data.roleList.map((item: { roleId: any }) => item.roleId);
dataForm.roleId = dataForm.roleList[0].roleId;
}
if (data.postList) {
dataForm.post = data.postList.map((item: { postId: any }) => item.postId);
}
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化部门数据
// 角色数据
const getRoleData = () => {
roleList().then((res) => {
roleData.value = res.data;
// 默认选择第一个
if (!dataForm.userId) {
dataForm.role = [res.data[0].roleId];
}
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,29 @@
export default {
sysuser: {
index: '#',
username: 'HR No.',
name: 'name',
phone: 'phone',
post: 'post',
role: 'role',
lockFlag: 'lockFlag',
createTime: 'createTime',
password: 'password',
dept: 'dept',
email: 'email',
nickname: 'nickname',
inputUsernameTip: 'input hr no.',
inputPhoneTip: 'input phone',
inputPasswordTip: 'input password',
inputNameTip: 'input name',
importUserTip: 'user import',
deleteDisabledTip: 'admin are not allowed to delete',
noDataScopeTip: 'no data permissions',
passwordBtn: 'password',
},
personal: {
name: 'personal info',
passwordRule: 'The two passwords are inconsistent',
passwordScore: 'Password level is too low',
},
};

View File

@@ -0,0 +1,29 @@
export default {
sysuser: {
index: '#',
username: 'HR编码',
name: '姓名',
phone: '手机号',
post: '岗位',
role: '角色',
lockFlag: '启用',
createTime: '创建时间',
password: '密码',
dept: '部门',
email: '邮箱',
nickname: '昵称',
inputUsernameTip: '请输入HR编码',
inputPhoneTip: '请输入手机号',
inputNameTip: '请输入姓名',
inputPasswordTip: '请输入6-20位合法密码',
importUserTip: '用户导入',
deleteDisabledTip: 'admin 不允许被删除',
noDataScopeTip: '没有数据权限',
passwordBtn: '密码',
},
personal: {
name: '个人信息',
passwordRule: '两次输入密码不一致',
passwordScore: '密码等级太低',
},
};

View File

@@ -0,0 +1,262 @@
<template>
<div class="layout-padding">
<splitpanes>
<pane size="15">
<div class="layout-padding-auto layout-padding-view">
<el-scrollbar>
<query-tree :placeholder="$t('common.queryDeptTip')" :query="deptData.queryList" :show-expand="true" @node-click="handleNodeClick">
<!-- 没有数据权限提示 -->
<template #default="{ node, data }">
<el-tooltip v-if="data.isLock" class="item" effect="dark" :content="$t('sysuser.noDataScopeTip')" placement="right-start">
<span
>{{ node.label }}
<SvgIcon name="ele-Lock" />
</span>
</el-tooltip>
<span v-if="!data.isLock">{{ node.label }}</span>
</template>
</query-tree>
</el-scrollbar>
</div>
</pane>
<pane class="ml8">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-form-item :label="$t('sysuser.username')" prop="username">
<el-input v-model="state.queryForm.username" :placeholder="$t('sysuser.inputUsernameTip')" clearable />
</el-form-item>
<el-form-item :label="$t('sysuser.name')" prop="name">
<el-input v-model="state.queryForm.name" :placeholder="$t('sysuser.inputNameTip')" clearable />
</el-form-item>
<!-- <el-form-item :label="$t('sysuser.phone')" prop="phone">
<el-input v-model="state.queryForm.phone" :placeholder="$t('sysuser.inputPhoneTip')" clearable />
</el-form-item> -->
<el-form-item>
<el-button icon="Search" type="primary" @click="getDataList">{{ $t('common.queryBtn') }}</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'sys_user_add'" icon="folder-add" type="primary" @click="userDialogRef.openDialog()">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain v-auth="'sys_user_add'" class="ml10" icon="upload-filled" type="primary" @click="excelUploadRef.show()">
{{ $t('common.importBtn') }}
</el-button>
<el-button
plain
v-auth="'sys_user_del'"
:disabled="multiple"
class="ml10"
icon="Delete"
type="primary"
@click="handleDelete(selectObjs)"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_user_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10 mr20"
style="float: right"
/>
</div>
</el-row>
<el-table
v-loading="state.loading"
:data="state.dataList"
@selection-change="handleSelectionChange"
row-key="userId"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column :selectable="handleSelectable" type="selection" width="40" />
<el-table-column :label="$t('sysuser.index')" type="index" width="60" fixed="left" />
<el-table-column :label="$t('sysuser.username')" prop="username" fixed="left" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysuser.name')" prop="name" show-overflow-tooltip></el-table-column>
<el-table-column label="公司" prop="companyName" show-overflow-tooltip></el-table-column>
<el-table-column label="部门" prop="deptName" show-overflow-tooltip></el-table-column>
<el-table-column label="岗位" prop="gangwei" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('sysuser.role')" show-overflow-tooltip>
<template #default="scope">
<el-tag v-for="(item, index) in scope.row.roleList" :key="index">{{ item.roleName }}</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('sysuser.lockFlag')" show-overflow-tooltip>
<template #default="scope">
<el-switch v-model="scope.row.lockFlag" @change="changeSwitch(scope.row)" active-value="0" inactive-value="9"></el-switch>
</template>
</el-table-column>
<el-table-column :label="$t('common.action')" width="200" fixed="right">
<template #default="scope">
<div style="display: flex">
<!-- 重置密码 -->
<popover-input v-model="inputPassword" @confirm="changePassword(scope.row)">
<template #default>
<el-button v-auth="'sys_user_edit'" icon="RefreshLeft" text type="primary" class="mr-4">
{{ $t('sysuser.passwordBtn') }}
</el-button>
</template>
</popover-input>
<!-- 修改信息 -->
<el-button v-auth="'sys_user_edit'" icon="edit-pen" text type="primary" @click="userDialogRef.openDialog(scope.row.userId)">
{{ $t('common.editBtn') }}
</el-button>
<!-- 删除用户 -->
<el-tooltip :content="$t('sysuser.deleteDisabledTip')" :disabled="scope.row.userId !== '1'" placement="top">
<span style="margin-left: 12px">
<el-button
icon="delete"
v-auth="'sys_user_del'"
:disabled="scope.row.username === 'admin'"
text
type="primary"
@click="handleDelete([scope.row.userId])"
>{{ $t('common.delBtn') }}
</el-button>
</span>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @current-change="currentChangeHandle" @size-change="sizeChangeHandle"> </pagination>
</div>
</pane>
</splitpanes>
<user-form ref="userDialogRef" @refresh="getDataList(false)" :deptId="state.queryForm.deptId" />
<upload-excel
ref="excelUploadRef"
:title="$t('sysuser.importUserTip')"
temp-url="/admin/sys-file/local/file/user.xlsx"
url="/admin/user/import"
@refreshDataList="getDataList"
/>
</div>
</template>
<script lang="ts" name="systemUser" setup>
import { delObj, pageList, putObj } from '/@/api/admin/user';
import { deptTree } from '/@/api/admin/dept';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 动态引入组件
const UserForm = defineAsyncComponent(() => import('./form.vue'));
const QueryTree = defineAsyncComponent(() => import('/@/components/QueryTree/index.vue'));
const PopoverInput = defineAsyncComponent(() => import('/@/components/PopoverInput/index.vue'));
const { t } = useI18n();
// 定义变量内容
const userDialogRef = ref();
const excelUploadRef = ref();
const queryRef = ref();
const showSearch = ref(true);
const inputPassword = ref();
// 多选rows
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
// 定义表格查询、后台调用的API
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
deptId: '',
username: '',
name: '',
},
pageList: pageList,
});
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 部门树使用的数据
const deptData = reactive({
queryList: async (name: String) => {
const res = await deptTree({ deptName: name });
return res.data || [];
},
});
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields();
state.queryForm.deptId = '';
getDataList();
};
// 点击树
const handleNodeClick = (e: any) => {
state.queryForm.deptId = e.id;
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/admin/user/export', Object.assign(state.queryForm, { ids: selectObjs }), 'users.xlsx');
};
// 是否可以多选
const handleSelectable = (row: any) => {
return row.username !== 'admin';
};
// 多选事件
const handleSelectionChange = (objs: { userId: string }[]) => {
selectObjs.value = objs.map(({ userId }) => userId);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
//表格内开关 (用户状态)
const changeSwitch = async (row: any) => {
// 不修改密码
row.password = undefined;
row.name = undefined;
await putObj(row);
useMessage().success(t('common.optSuccessText'));
getDataList();
};
//修改用户密码
const changePassword = async (row: any) => {
if (!inputPassword.value || inputPassword.value.length < 6 || inputPassword.value.length > 20) {
useMessage().error(t('sysuser.inputPasswordTip'));
return;
}
row.name = undefined;
row.password = inputPassword.value;
await putObj(row);
useMessage().success(t('common.optSuccessText'));
getDataList();
};
</script>

View File

@@ -0,0 +1,565 @@
<template>
<el-drawer v-model="visible" :title="$t('personal.name')" size="40%">
<el-tabs style="height: 200px" class="demo-tabs">
<el-tab-pane label="基本信息" v-loading="loading">
<template #label>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
基本信息
</template>
<el-form :model="formData" :rules="ruleForm" label-width="100px" class="mt30" ref="formdataRef">
<el-row :gutter="20">
<el-col :span="24" class="mb20">
<el-form-item prop="avatar">
<ImageUpload v-model:imageUrl="formData.avatar" borderRadius="50%">
<template #empty>
<el-icon><Avatar /></el-icon>
<span>请上传头像</span>
</template>
</ImageUpload>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username" clearable disabled></el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="手机" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入手机" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="昵称" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入昵称" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item>
<el-button type="primary" @click="handleSaveUser"> 更新个人信息 </el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<el-tab-pane label="安全信息">
<template #label>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
</svg>
安全信息
</template>
<el-form :model="passwordFormData" :rules="passwordRuleForm" label-width="100px" class="mt30" ref="passwordFormdataRef">
<el-row :gutter="20">
<el-col :span="24" class="mb20">
<el-form-item label="原密码" prop="password">
<el-input v-model="passwordFormData.password" :type="showPassword ? 'text' : 'password'" placeholder="请输入密码" clearable type="password">
<template #suffix>
<i
class="iconfont el-input__icon login-content-password"
:class="showPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
@click="showPassword = !showPassword"
>
</i>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="新密码" prop="newpassword1">
<strength-meter
v-model="passwordFormData.newpassword1"
:minlength="6"
:maxlength="16"
placeholder="请输入新密码"
@score="passwordScore"
></strength-meter>
<!-- <el-input v-model="passwordFormData.newpassword1" clearable type="password"></el-input>-->
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item label="确认密码" prop="newpassword2">
<strength-meter v-model="passwordFormData.newpassword2" :minlength="6" :maxlength="16" placeholder="请重复密码"></strength-meter>
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item>
<el-button type="primary" @click="handleChangePassword"> 修改密码 </el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-tab-pane>
<el-tab-pane label="第三方账号">
<template #label>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0 1 19.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 0 0 4.5 10.5a7.464 7.464 0 0 1-1.15 3.993m1.989 3.559A11.209 11.209 0 0 0 8.25 10.5a3.75 3.75 0 1 1 7.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 0 1-3.6 9.75m6.633-4.596a18.666 18.666 0 0 1-2.485 5.33" />
</svg>
社交登录
</template>
<el-table :data="socialList" class="mt10">
<el-table-column type="index" label="序号" width="80"></el-table-column>
<el-table-column prop="name" label="平台"></el-table-column>
<el-table-column label="状态">
<template #default="scope">
<el-tag v-if="scope.row.openId"> 已绑定 </el-tag>
<el-tag v-else> 未绑定 </el-tag>
</template>
</el-table-column>
<el-table-column prop="action" label="操作">
<template #default="scope">
<el-button @click="unbinding(scope.row.type)" text type="primary" v-if="scope.row.openId"> 解绑 </el-button>
<el-button @click="handleClick(scope.row.type)" text type="primary" v-else> 绑定 </el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</el-drawer>
</template>
<script setup lang="ts" name="personal">
import {useUserInfo} from '/@/stores/userInfo';
import {editInfo, getObj, password, unbindingUser} from '/@/api/admin/user';
import {useMessage} from '/@/hooks/message';
import {rule, validateNull} from '/@/utils/validate';
import other from '/@/utils/other';
import {Session} from '/@/utils/storage';
import {useI18n} from 'vue-i18n';
import {getLoginAppList} from "/@/api/admin/social";
import { SocialLoginEnum } from '/@/api/login';
const { t } = useI18n();
const ImageUpload = defineAsyncComponent(() => import('/@/components/Upload/Image.vue'));
const StrengthMeter = defineAsyncComponent(() => import('/@/components/StrengthMeter/index.vue'));
const visible = ref(false);
// 定义变量内容
const formData = ref({
userId: '',
username: '',
name: '',
email: '',
avatar: '',
nickname: '',
wxDingUserid: '',
wxCpUserid: '',
phone: ('' as string) || undefined,
});
const showPassword = ref(false);
const passwordFormData = reactive({
password: '',
newpassword1: '',
newpassword2: '',
});
const formdataRef = ref();
const passwordFormdataRef = ref();
const ruleForm = reactive({
phone: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{ validator: rule.validatePhone, trigger: 'blur' },
],
nickname: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '昵称不能为空', trigger: 'blur' }],
email: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '邮箱不能为空', trigger: 'blur' }],
name: [{ validator: rule.overLength, trigger: 'blur' },{ required: true, message: '姓名不能为空', trigger: 'blur' }],
});
const validatorPassword2 = (rule: any, value: any, callback: any) => {
if (value !== passwordFormData.newpassword1) {
callback(new Error(t('personal.passwordRule')));
} else {
callback();
}
};
const validatorScore = (rule: any, value: any, callback: any) => {
if (score.value <= 1) {
callback(new Error(t('personal.passwordScore')));
} else {
callback();
}
};
const passwordRuleForm = reactive({
password: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
newpassword1: [
{
min: 6,
max: 20,
message: '用户密码长度必须介于 6 和 20 之间',
trigger: 'blur',
},
{ validator: validatorScore, trigger: 'blur' },
],
newpassword2: [
{
min: 6,
max: 20,
message: '用户密码长度必须介于 6 和 20 之间',
trigger: 'blur',
},
{ validator: validatorPassword2, trigger: 'blur' },
],
});
const score = ref(0);
const passwordScore = (e: any) => {
score.value = e;
};
const handleChangePassword = () => {
passwordFormdataRef.value.validate((valid: boolean) => {
if (!valid) {
return false;
}
password(passwordFormData)
.then(() => {
useMessage().success('修改成功');
// 需要重新登录
// 清除缓存/token等
Session.clear();
// 使用 reload 时,不需要调用 resetRoute() 重置路由
window.location.reload();
})
.catch((err) => {
useMessage().error(err.msg);
});
});
};
// 保存用户
const handleSaveUser = () => {
formdataRef.value.validate((valid: boolean) => {
if (!valid) {
return false;
}
if (formData.value.phone && formData.value.phone.includes('*')) {
formData.value.phone = undefined;
}
editInfo(formData.value)
.then(() => {
useMessage().success('修改成功');
// 更新上下文的 user信息
useUserInfo().setUserInfos();
})
.catch((err) => {
useMessage().error(err.msg);
});
});
};
const socialList = ref([] as any);
const initSocialList = () => {
socialList.value = [
{
name: '企业微信',
type: SocialLoginEnum.WEIXIN_CP,
openId: formData.value.wxCpUserid,
},
{
name: '钉钉办公',
type: SocialLoginEnum.DINGTALK,
openId: formData.value.wxDingUserid,
},
];
};
const handleClick = async (thirdpart: SocialLoginEnum) => {
// 获取租户配置的账号信息
const { data } = await getLoginAppList();
const result = data.find((item: any) => item.type === thirdpart);
if (validateNull(result)) {
useMessage().error(t('scan.appErrorTip'));
return;
}
let redirect_uri, url;
redirect_uri = encodeURIComponent(window.location.origin + '/#/authredirect');
if (thirdpart === SocialLoginEnum.WEIXIN_CP) {
url = `https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=${result.appId}&agentid=${result.ext}&redirect_uri=${redirect_uri}&state=${SocialLoginEnum.WEIXIN_CP}-BIND`;
}
if (thirdpart === SocialLoginEnum.DINGTALK) {
url = `https://login.dingtalk.com/oauth2/auth?redirect_uri=${redirect_uri}&response_type=code&client_id=${result.appId}&scope=openid&state=${SocialLoginEnum.DINGTALK}-BIND&prompt=consent`;
}
if (url) {
other.openWindow(url, thirdpart, 540, 540);
}
};
const unbinding = (type: SocialLoginEnum) => {
unbindingUser(type)
.then(() => {
useMessage().success('解绑成功');
})
.catch((err) => {
useMessage().error(err.msg);
})
.finally(() => {
initUserInfo(formData.value.userId);
});
};
const open = () => {
visible.value = true;
const data = useUserInfo().userInfos;
initUserInfo(data.user.userId);
// Object.assign(formData, data.user);
};
const loading = ref(false);
const initUserInfo = (userId: any) => {
loading.value = true;
getObj(userId)
.then((res) => {
formData.value = res.data;
initSocialList();
})
.catch((err) => {
useMessage().error(err.msg);
})
.finally(() => {
loading.value = false;
});
};
// 暴露变量
defineExpose({
open,
});
</script>
<style scoped lang="scss">
@import '/@/theme/mixins/index.scss';
.personal {
.personal-user {
height: 130px;
display: flex;
align-items: center;
.personal-user-left {
width: 180px;
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;
}
}
}
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 100%;
}
.item {
display: flex;
flex-direction: column;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,14 @@
export default {
systoken: {
index: '#',
userId: 'userId',
username: 'username',
clientId: 'clientId',
accessToken: 'accessToken',
expiresAt: 'expiresAt',
inputUsernameTip: 'input Username',
offlineBtn: 'offline',
offlineConfirmText: 'offline confirm',
offlineSuccessText: 'offline success',
},
};

View File

@@ -0,0 +1,14 @@
export default {
systoken: {
index: '#',
userId: '用户ID',
username: '用户名',
clientId: '客户端',
accessToken: '令牌',
expiresAt: '过期时间',
inputUsernameTip: '请输入用户名',
offlineBtn: '下线',
offlineConfirmText: '确认下线',
offlineSuccessText: '下线成功',
},
};

View File

@@ -0,0 +1,122 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('systoken.username')" prop="username">
<el-input :placeholder="$t('systoken.inputUsernameTip')" v-model="state.queryForm.username"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="Search" type="primary">{{ $t('common.queryBtn') }} </el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button :disabled="multiple" @click="handleDelete(selectObjs)" class="ml10" icon="Delete" type="primary" v-auth="'sys_user_del'">
{{ $t('systoken.offlineBtn') }}
</el-button>
<right-toolbar
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
style="width: 100%"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="$t('systoken.index')" type="index" width="60" />
<el-table-column :label="$t('systoken.username')" prop="username" show-overflow-tooltip width="150"></el-table-column>
<el-table-column :label="$t('systoken.clientId')" prop="clientId" show-overflow-tooltip width="100"></el-table-column>
<el-table-column :label="$t('systoken.accessToken')" prop="accessToken" show-overflow-tooltip>
<template #default="scope">
<el-button link type="danger" v-if="filterOwnToken(scope.row)">
{{ scope.row.accessToken }}
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('systoken.expiresAt')" prop="expiresAt" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" width="100">
<template #default="scope">
<el-button icon="delete" @click="handleDelete([scope.row.accessToken])" size="small" text type="primary" v-auth="'sys_user_del'">
{{ $t('systoken.offlineBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination"> </pagination>
</div>
</div>
</template>
<script lang="ts" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchList } from '/@/api/admin/token';
import { useI18n } from 'vue-i18n';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { Session } from '/@/utils/storage';
const { t } = useI18n();
// 定义变量内容
const queryRef = ref();
const showSearch = ref(true);
// 多选rows
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
// table hook
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
username: '',
},
pageList: fetchList,
});
const { getDataList, currentChangeHandle, sortChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields();
getDataList();
};
// 多选事件
const handleSelectionChange = (objs: { accessToken: string }[]) => {
selectObjs.value = objs.map(({ accessToken }) => accessToken);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (accessTokens: string[]) => {
try {
await useMessageBox().confirm(t('systoken.offlineConfirmText'));
} catch {
return; // 取消删除则直接跳过此方法
}
try {
await delObj(accessTokens);
getDataList();
useMessage().success(t('systoken.offlineSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
// 判断当前token 是否和登录token一致
const filterOwnToken = (row: any) => {
return Session.getToken() === row.accessToken;
};
</script>

View File

@@ -0,0 +1,204 @@
<template>
<div class="layout-padding-auto layout-padding-view">
<el-card shadow="never">
<el-form ref="dataFormRef" class="form" :model="form" label-width="85px" :rules="dataRules">
<div class="flex">
<div>
<el-form-item label="文章标题" prop="title">
<div class="w-80">
<el-input
v-model="form.title"
placeholder="请输入文章标题"
type="textarea"
:autosize="{ minRows: 3, maxRows: 3 }"
maxlength="64"
show-word-limit
clearable
/>
</div>
</el-form-item>
<el-form-item label="文章简介" prop="intro">
<div class="w-80">
<el-input
v-model="form.intro"
placeholder="请输入文章简介"
type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }"
:maxlength="200"
show-word-limit
clearable
/>
</div>
</el-form-item>
<el-form-item label="摘要" prop="summary">
<div class="w-80">
<el-input type="textarea" :autosize="{ minRows: 6, maxRows: 6 }" v-model="form.summary" maxlength="200"
show-word-limit clearable/>
</div>
</el-form-item>
<el-form-item label="文章封面" prop="image">
<div>
<div>
<upload-img v-model:imageUrl="form.image"/>
</div>
<div class="form-tips">建议尺寸240*180px</div>
</div>
</el-form-item>
</div>
<div class="xl:ml-20">
<el-row class="xl:mb-8">
<el-col :span="12">
<el-form-item label="文章栏目" prop="cid">
<el-select class="w-80" v-model="form.cid" placeholder="请选择文章栏目" clearable>
<el-option v-for="item in articleCateList" :key="item.id" :label="item.name" :value="item.id"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="作者" prop="author">
<div class="w-80">
<el-input v-model="form.author" placeholder="请输入作者名称"/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row class="xl:mb-8">
<el-col :span="12">
<el-form-item label="初始浏览量" prop="visit">
<template #label> 浏览量
<tip content="初始值"/>
</template>
<el-input-number class="!w-80" v-model="form.visit" :min="0" :max="9999"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="sort">
<template #label> 排序
<tip content="默认为0 数值越大越排前"/>
</template>
<el-input-number class="!w-80" v-model="form.sort" :min="0" :max="9999"/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="文章内容" required prop="content">
<editor v-model:get-html="form.content" height="500" width="600" :disable="form.id !== ''"/>
</el-form-item>
<div style="text-align: center">
<el-button type="primary" @click="onSubmit">保存</el-button>
</div>
</div>
</div>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts" name="AppArticleDialog">
import mittBus from "/@/utils/mitt";
import {useMessage} from '/@/hooks/message';
import {getObj, addObj, putObj} from '/@/api/app/appArticle';
import {getObjList} from '/@/api/app/appArticleCategory';
const emit = defineEmits(['refresh']);
const route = useRoute();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 定义字典
const articleCateList = ref([]);
// 提交表单数据
const form = reactive({
id: '',
cid: '',
title: '',
intro: '',
summary: '',
image: '',
content: '',
author: '',
visit: 0,
sort: 0,
});
// 定义校验规则
const dataRules = ref({
cid: [{required: true, message: '分类不能为空', trigger: 'blur'}],
title: [{required: true, message: '标题不能为空', trigger: 'blur'}],
intro: [{required: true, message: '简介不能为空', trigger: 'blur'}],
summary: [{required: true, message: '摘要不能为空', trigger: 'blur'}],
image: [{required: true, message: '封面不能为空', trigger: 'blur'}],
content: [{required: true, message: '内容不能为空', trigger: 'blur'}],
author: [{required: true, message: '作者不能为空', trigger: 'blur'}],
visit: [{required: true, message: '浏览不能为空', trigger: 'blur'}],
});
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {
});
if (!valid) return false;
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
useMessage().success(form.id ? '修改成功' : '添加成功');
visible.value = false;
emit('refresh');
// 关闭当前tab
mittBus.emit(
"onCurrentContextmenuClick",
Object.assign({}, {contextMenuClickId: 1, ...route})
);
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getAppArticleData = (id: string) => {
// 获取数据
loading.value = true;
getObj(id)
.then((res: any) => {
Object.assign(form, res.data);
})
.finally(() => {
loading.value = false;
});
};
// 查询全部的分类
const getAppCateList = () => {
getObjList().then((res: any) => {
articleCateList.value = res.data;
});
};
onMounted(() => {
getAppCateList();
if (route.query?.id) {
getAppArticleData(route.query?.id);
}
});
</script>
<style scoped lang="scss">
.footer-btns {
height: 60px;
&__content {
bottom: 0;
height: 60px;
right: 0;
left: 0;
z-index: 99;
@apply flex justify-center items-center shadow;
}
}
</style>

View File

@@ -0,0 +1,6 @@
export default {
article: {
edit: 'edit article',
add: 'add article',
},
};

View File

@@ -0,0 +1,6 @@
export default {
article: {
edit: '编辑文章',
add: '发布文章',
},
};

View File

@@ -0,0 +1,133 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item label="标题" prop="title">
<el-input placeholder="请输入标题" v-model="state.queryForm.title" />
</el-form-item>
<el-form-item label="作者" prop="author">
<el-input placeholder="请输入作者" v-model="state.queryForm.author" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList"> 查询 </el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'app_appArticle_add'" icon="folder-add" type="primary" class="ml10" @click="addOrUpdate()"> </el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'app_appArticle_del'" @click="handleDelete(selectObjs)">
删除
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'app_appArticle_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="cname" label="分类" show-overflow-tooltip />
<el-table-column prop="title" label="标题" show-overflow-tooltip />
<el-table-column prop="author" label="作者" show-overflow-tooltip />
<el-table-column prop="visit" label="浏览" show-overflow-tooltip />
<el-table-column prop="sort" label="排序" show-overflow-tooltip />
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" v-auth="'app_appArticle_edit'" @click="addOrUpdate(scope.row.id)">编辑</el-button>
<el-button icon="delete" text type="primary" v-auth="'app_appArticle_del'" @click="handleDelete([scope.row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</div>
</template>
<script setup lang="ts" name="systemAppArticle">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObjs } from '/@/api/app/appArticle';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 定义查询字典
// 定义变量内容
const router = useRouter();
const { t } = useI18n();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/app/appArticle/export', state.queryForm, 'appArticle.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm('此操作将永久删除');
} catch {
return;
}
try {
await delObjs(ids);
getDataList();
useMessage().success('删除成功');
} catch (err: any) {
useMessage().error(err.msg);
}
};
// 跳转发布页
const addOrUpdate = (id?: string) => {
const tagsViewName = id ? `${t('article.edit')}:${id}` : t('article.add');
router.push({
path: '/biz/app/appArticle/form',
query: { id: id, tagsViewName: tagsViewName },
});
};
</script>

View File

@@ -0,0 +1,104 @@
<template>
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" width="600" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number :min="1" :max="1000" v-model="form.sort" placeholder="请输入排序"></el-input-number>
</el-form-item>
<el-form-item label="是否显示" prop="isShow">
<el-radio-group v-model="form.isShow">
<el-radio :label="Number(item.value)" v-for="(item, index) in yes_no_type" border :key="index">{{ item.label }} </el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="AppArticleCategoryDialog">
import { useDict } from '/@/hooks/dict';
import { useMessage } from '/@/hooks/message';
import { getObj, addObj, putObj } from '/@/api/app/appArticleCategory';
const emit = defineEmits(['refresh']);
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 定义字典
const { yes_no_type } = useDict('yes_no_type');
// 提交表单数据
const form = reactive({
id: '',
name: '',
sort: 0,
isShow: 1,
});
// 定义校验规则
const dataRules = ref({
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
isShow: [{ required: true, message: '是否显示', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.id = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取appArticleCategory信息
if (id) {
form.id = id;
getappArticleCategoryData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
useMessage().success(form.id ? '修改成功' : '添加成功');
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getappArticleCategoryData = (id: string) => {
// 获取数据
loading.value = true;
getObj(id)
.then((res: any) => {
Object.assign(form, res.data);
})
.finally(() => {
loading.value = false;
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,124 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item label="名称" prop="name">
<el-input placeholder="请输入名称" v-model="state.queryForm.name" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList"> 查询 </el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()" v-auth="'app_appArticleCategory_add'">
</el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'app_appArticleCategory_del'" @click="handleDelete(selectObjs)">
删除
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'app_appArticleCategory_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="name" label="名称" show-overflow-tooltip />
<el-table-column prop="sort" label="排序" show-overflow-tooltip />
<el-table-column prop="isShow" label="是否显示" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="yes_no_type" :value="scope.row.isShow"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" v-auth="'app_appArticleCategory_edit'" @click="formDialogRef.openDialog(scope.row.id)"
>编辑</el-button
>
<el-button icon="delete" text type="primary" v-auth="'app_appArticleCategory_del'" @click="handleDelete([scope.row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
</div>
</template>
<script setup lang="ts" name="systemAppArticleCategory">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObjs } from '/@/api/app/appArticleCategory';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
// 定义查询字典
const { yes_no_type } = useDict('yes_no_type');
// 定义变量内容
const formDialogRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/app/appArticleCategory/export', state.queryForm, 'appArticleCategory.xlsx');
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm('此操作将永久删除');
} catch {
return;
}
try {
await delObjs(ids);
getDataList();
useMessage().success('删除成功');
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,135 @@
<template>
<el-dialog :close-on-click-modal="false" :title="form.roleId ? $t('common.editBtn') : $t('common.addBtn')" width="600" draggable v-model="visible">
<el-form :model="form" :rules="dataRules" label-width="90px" ref="dataFormRef" v-loading="loading">
<el-form-item :label="$t('approle.roleName')" prop="roleName">
<el-input :placeholder="$t('approle.please_enter_a_role_name')" clearable v-model="form.roleName"></el-input>
</el-form-item>
<el-form-item :label="$t('approle.roleCode')" prop="roleCode">
<el-input :placeholder="$t('approle.please_enter_the_role_Code')" clearable v-model="form.roleCode"></el-input>
</el-form-item>
<el-form-item :label="$t('approle.roleDesc')" prop="roleDesc">
<el-input :placeholder="$t('approle.please_enter_the_role_description')" maxlength="150" type="textarea" v-model="form.roleDesc"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="systemRoleDialog" setup>
import { rule } from '/@/utils/validate';
import { useMessage } from '/@/hooks/message';
import { addObj, getObj, putObj, validateAppRoleCode, validateApproleName } from '/@/api/app/approle';
import { useI18n } from 'vue-i18n';
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 提交表单数据
const form = reactive({
roleId: '',
roleName: '',
roleCode: '',
roleDesc: '',
});
// 页面对应元数据
const dataForm = reactive({
deptData: [],
checkedDsScope: [],
deptProps: {
children: 'children',
label: 'name',
value: 'id',
},
});
// 定义校验规则
const dataRules = ref({
roleName: [
{ required: true, message: '角色名称不能为空', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateApproleName(rule, value, callback, form.roleId !== '');
},
trigger: 'blur',
},
],
roleCode: [
{ required: true, message: '角色标识不能为空', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
{ validator: rule.validatorCapital, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateAppRoleCode(rule, value, callback, form.roleId !== '');
},
trigger: 'blur',
},
],
roleDesc: [{ max: 128, message: '长度在 128 个字符内', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.roleId = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取角色信息
if (id) {
form.roleId = id;
getRoleData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
form.roleId ? await putObj(form) : await addObj(form);
useMessage().success(t(form.roleId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化角色数据
const getRoleData = (id: string) => {
// 获取部门数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
if (res.data.dsScope) {
dataForm.checkedDsScope = res.data.dsScope.split(',');
} else {
dataForm.checkedDsScope = [];
}
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,15 @@
export default {
approle: {
index: '#',
roleName: 'roleName',
inputRoleNameTip: 'input roleName',
permissionTip: 'grant',
roleCode: 'roleCode',
roleDesc: 'role description',
createTime: 'createTime',
please_enter_a_role_name: 'please enter a role name',
please_enter_the_role_Code: 'please enter the role Code',
please_enter_the_role_description: 'please enter the role description',
},
};

View File

@@ -0,0 +1,15 @@
export default {
approle: {
index: '#',
roleName: '角色名',
inputRoleNameTip: '请输入角色名称',
permissionTip: '授权',
roleCode: '用户标识',
roleDesc: '用户描述',
createTime: '创建时间',
please_enter_a_role_name: '请输入角色名称',
please_enter_the_role_Code: '请输入角色标识',
please_enter_the_role_description: '请输入角色描述',
},
};

View File

@@ -0,0 +1,153 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('approle.roleName')" prop="roleName">
<el-input :placeholder="$t('approle.inputRoleNameTip')" style="max-width: 180px" v-model="state.queryForm.roleName" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="roleDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary" v-auth="'app_approle_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain @click="excelUploadRef.show()" class="ml10" icon="upload-filled" type="primary" v-auth="'app_approle_export'">
{{ $t('common.importBtn') }}
</el-button>
<el-button
plain
:disabled="multiple"
@click="handleDelete(selectObjs)"
class="ml10"
icon="Delete"
type="primary"
v-auth="'app_approle_del'"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'app_approle_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
style="width: 100%"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="$t('approle.index')" type="index" width="60" />
<el-table-column :label="$t('approle.roleName')" prop="roleName" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('approle.roleCode')" prop="roleCode" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('approle.roleDesc')" prop="roleDesc" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('approle.createTime')" prop="createTime" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" width="250">
<template #default="scope">
<el-button icon="edit-pen" @click="roleDialogRef.openDialog(scope.row.roleId)" text type="primary" v-auth="'app_approle_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" @click="handleDelete([scope.row.roleId])" text type="primary" v-auth="'app_approle_del'"
>{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
</div>
<!-- 角色编辑新增 -->
<role-dialog @refresh="getDataList()" ref="roleDialogRef" />
<!-- 导入角色 -->
<upload-excel
:title="$t('sysuser.importUserTip')"
@refreshDataList="getDataList"
ref="excelUploadRef"
temp-url="/admin/sys-file/local/file/approle.xlsx"
url="/admin/approle/import"
/>
</div>
</template>
<script lang="ts" name="systemRole" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchList } from '/@/api/app/approle';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 引入组件
const RoleDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义变量内容
const roleDialogRef = ref();
const excelUploadRef = ref();
const queryRef = ref();
const showSearch = ref(true);
// 多选rows
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
roleName: '',
},
pageList: fetchList, // H
descs: ['create_time'],
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/app/approle/export', state.queryForm, 'approle.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { roleId: string }[]) => {
selectObjs.value = objs.map(({ roleId }) => roleId);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,151 @@
<template>
<el-dialog :close-on-click-modal="false" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" draggable v-model="visible">
<el-form :model="form" :rules="dataRules" formDialogRef label-width="90px" ref="dataFormRef" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="t('appsocial.type')" prop="type">
<el-select :placeholder="t('appsocial.inputTypeTip')" v-model="form.type">
<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in app_social_type"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('appsocial.remark')" prop="remark">
<el-input :placeholder="t('appsocial.inputRemarkTip')" v-model="form.remark" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('appsocial.appId')" prop="appId">
<el-input :placeholder="t('appsocial.inputAppIdTip')" v-model="form.appId" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('appsocial.appSecret')" prop="appSecret">
<el-input :placeholder="t('appsocial.inputAppSecretTip')" v-model="form.appSecret" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('appsocial.redirectUrl')" prop="redirectUrl">
<el-input :placeholder="t('appsocial.inputRedirectUrlTip')" type="textarea" v-model="form.redirectUrl" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('appsocial.ext')" prop="ext">
<el-input :placeholder="t('appsocial.inputExtTip')" type="textarea" v-model="form.ext" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="AppSocialDetailsDialog" setup>
// 定义子组件向父组件传值/事件
import { useDict } from '/@/hooks/dict';
import { useMessage } from '/@/hooks/message';
import { addObj, getObj, putObj } from '/@/api/app/appsocial';
import { useI18n } from 'vue-i18n';
import { rule } from '/@/utils/validate';
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 定义字典
const { app_social_type } = useDict('app_social_type');
// 提交表单数据
const form = reactive({
id: '',
type: '',
remark: '',
appId: '' as string | undefined,
appSecret: '' as string | undefined,
redirectUrl: '',
ext: '',
});
// 定义校验规则
const dataRules = ref({
type: [{ required: true, message: '类型不能为空', trigger: 'blur' }],
appId: [{ required: true, message: 'appId不能为空', trigger: 'blur' }],
appSecret: [{ required: true, message: 'app秘钥不能为空', trigger: 'blur' }],
redirectUrl: [
{ required: true, message: '回调地址不能为空', trigger: 'blur' },
{ validator: rule.url, trigger: 'blur' },
],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.id = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取appSocialDetails信息
if (id) {
form.id = id;
getappSocialDetailsData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
// 隐藏敏感信息
form.appSecret = form.appSecret?.includes('******') ? undefined : form.appSecret;
form.appId = form.appId?.includes('******') ? undefined : form.appId;
try {
loading.value = true;
if (form.id) {
await putObj(form);
useMessage().success(t('common.editSuccessText'));
} else {
await addObj(form);
useMessage().success(t('common.addSuccessText'));
}
visible.value = false; // 关闭弹窗
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getappSocialDetailsData = (id: string) => {
// 获取数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,32 @@
export default {
appsocial: {
index: '#',
importappSocialDetailsTip: 'import AppSocialDetails',
id: 'id',
type: 'type',
remark: 'remark',
appId: 'appId',
appSecret: 'appSecret',
redirectUrl: 'redirectUrl',
ext: 'ext',
createBy: 'createBy',
updateBy: 'updateBy',
createTime: 'createTime',
updateTime: 'updateTime',
delFlag: 'delFlag',
tenantId: 'tenantId',
inputIdTip: 'input id',
inputTypeTip: 'input type',
inputRemarkTip: 'input remark',
inputAppIdTip: 'input appId',
inputAppSecretTip: 'input appSecret',
inputRedirectUrlTip: 'input redirectUrl',
inputExtTip: 'input ext',
inputCreateByTip: 'input createBy',
inputUpdateByTip: 'input updateBy',
inputCreateTimeTip: 'input createTime',
inputUpdateTimeTip: 'input updateTime',
inputDelFlagTip: 'input delFlag',
inputTenantIdTip: 'input tenantId',
},
};

View File

@@ -0,0 +1,32 @@
export default {
appsocial: {
index: '#',
importappSocialDetailsTip: '导入系统社交登录账号表',
id: '主鍵',
type: '类型',
remark: '描述',
appId: 'appId',
appSecret: 'app秘钥',
redirectUrl: '回调地址',
ext: '拓展字段',
createBy: '创建人',
updateBy: '修改人',
createTime: '创建时间',
updateTime: '更新时间',
delFlag: '${field.fieldComment}',
tenantId: '所属租户',
inputIdTip: '请输入主鍵',
inputTypeTip: '请输入类型',
inputRemarkTip: '请输入描述',
inputAppIdTip: '请输入appId',
inputAppSecretTip: '请输入appSecret',
inputRedirectUrlTip: '请输入回调地址',
inputExtTip: '请输入拓展字段',
inputCreateByTip: '请输入创建人',
inputUpdateByTip: '请输入修改人',
inputCreateTimeTip: '请输入创建时间',
inputUpdateTimeTip: '请输入更新时间',
inputDelFlagTip: '请输入${field.fieldComment}',
inputTenantIdTip: '请输入所属租户',
},
};

View File

@@ -0,0 +1,162 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="t('appsocial.type')" class="ml2" prop="type">
<el-select :placeholder="t('appsocial.inputTypeTip')" v-model="state.queryForm.type">
<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in app_social_type"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="getDataList" formDialogRef icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" formDialogRef icon="Refresh">{{ $t('common.resetBtn') }} </el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button
@click="formDialogRef.openDialog()"
class="ml10"
formDialogRef
icon="folder-add"
type="primary"
v-auth="'app_social_details_add'"
>
{{ $t('common.addBtn') }}
</el-button>
<el-button
plain
:disabled="multiple"
@click="handleDelete(selectObjs)"
class="ml10"
formDialogRef
icon="Delete"
type="primary"
v-auth="'app_social_details_del'"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'app_social_details_del'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
style="width: 100%"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="t('appsocial.index')" type="index" width="60" />
<el-table-column :label="t('appsocial.type')" prop="type" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="app_social_type" :value="scope.row.type"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="t('appsocial.remark')" prop="remark" show-overflow-tooltip />
<el-table-column :label="t('appsocial.appId')" prop="appId" show-overflow-tooltip />
<el-table-column :label="t('appsocial.appSecret')" prop="appSecret" show-overflow-tooltip />
<el-table-column :label="t('appsocial.createTime')" prop="createTime" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" @click="formDialogRef.openDialog(scope.row.id)" text type="primary" v-auth="'app_social_details_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" @click="handleDelete([scope.row.id])" text type="primary" v-auth="'app_social_details_del'"
>{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog @refresh="getDataList()" ref="formDialogRef" />
</div>
</template>
<script lang="ts" name="systemAppSocialDetails" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchList } from '/@/api/app/appsocial';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
import { useI18n } from 'vue-i18n';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义查询字典
const { app_social_type } = useDict('app_social_type');
// 定义变量内容
const formDialogRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
type: '',
},
pageList: fetchList,
descs: ['create_time'],
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/app/appsocial/export', state.queryForm, 'appsocial.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,218 @@
<template>
<div class="system-user-dialog-container">
<el-dialog v-model="visible" :close-on-click-modal="false" :title="dataForm.userId ? $t('common.editBtn') : $t('common.addBtn')" draggable>
<el-form ref="dataFormRef" v-loading="loading" :model="dataForm" :rules="dataRules" label-width="90px">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.username')" prop="username">
<el-input v-model="dataForm.username" :disabled="dataForm.userId !== ''" :placeholder="$t('appuser.inputUserNameTip')"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.password')" prop="password">
<el-input v-model="dataForm.password" :placeholder="$t('appuser.inputPasswordTip')" clearable type="password"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.name')" prop="name">
<el-input v-model="dataForm.name" :placeholder="$t('appuser.inputNameTip')" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.phone')" prop="phone">
<el-input v-model="dataForm.phone" :placeholder="$t('appuser.inputPhoneTip')" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.role')" prop="role">
<el-select v-model="dataForm.role" :placeholder="$t('appuser.inputRoleTip')" clearable multiple>
<el-option v-for="item in roleData" :key="item.roleId" :label="item.roleName" :value="item.roleId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.email')" prop="email">
<el-input v-model="dataForm.email" :placeholder="$t('appuser.inputEmailTip')" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.nickname')" prop="nickname">
<el-input v-model="dataForm.nickname" :placeholder="$t('appuser.inputNickNameTip')" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.lockFlag')" prop="lockFlag">
<el-radio-group v-model="dataForm.lockFlag">
<el-radio v-for="(item, index) in lock_flag" :key="index" :label="item.value" border>{{ item.label }} </el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" name="systemUserDialog" setup>
import { addObj, getObj, putObj, validatePhone, validateUsername } from '/@/api/app/appuser';
import { list as roleList } from '/@/api/app/approle';
import { useDict } from '/@/hooks/dict';
import { useI18n } from 'vue-i18n';
import { useMessage } from '/@/hooks/message';
import { rule } from '/@/utils/validate';
const { t } = useI18n();
// 定义刷新表格emit
const emit = defineEmits(['refresh']);
// @ts-ignore
const { lock_flag } = useDict('lock_flag');
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const roleData = ref<any[]>([]);
const loading = ref(false);
const dataForm = reactive({
userId: '',
username: '',
password: '' as String | undefined,
salt: '',
wxOpenid: '',
qqOpenid: '',
lockFlag: '0',
phone: '' as String | undefined,
deptId: '',
roleList: [],
postList: [],
nickname: '',
name: '',
email: '',
post: [] as string[],
role: [] as string[],
});
const dataRules = ref({
username: [
{ required: true, message: '用户名不能为空', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名称长度必须介于 3 和 20 之间', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateUsername(rule, value, callback, dataForm.userId !== '');
},
trigger: 'blur',
},
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' },
{
min: 6,
max: 20,
message: '用户密码长度必须介于 6 和 20 之间',
trigger: 'blur',
},
],
name: [
{ required: true, message: '姓名不能为空', trigger: 'blur' },
{ validator: rule.chinese, trigger: 'blur' },
],
role: [{ required: true, message: '角色不能为空', trigger: 'blur' }],
phone: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{ validator: rule.validatePhone, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validatePhone(rule, value, callback, dataForm.userId !== '');
},
trigger: 'blur',
},
],
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }],
nickname: [{ required: true, message: '姓名不能为空', nickname: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
dataForm.userId = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value.resetFields();
});
// 修改获取用户信息
if (id) {
dataForm.userId = id;
getUserData(id);
}
getRoleData();
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
const { userId, phone, password } = dataForm;
if (userId) {
// 清除占位符,避免提交错误的数据
if (phone?.includes('*')) dataForm.phone = undefined;
if (password?.includes('******')) dataForm.password = undefined;
loading.value = true;
await putObj(dataForm);
useMessage().success(t('common.editSuccessText'));
visible.value = false; // 关闭弹窗
emit('refresh');
} else {
loading.value = true;
await addObj(dataForm);
useMessage().success(t('common.addSuccessText'));
visible.value = false; // 关闭弹窗
emit('refresh');
}
} catch (error: any) {
useMessage().error(error.msg);
} finally {
loading.value = false;
}
};
// 初始化用户信息数据
const getUserData = async (id: string) => {
loading.value = true;
try {
const { data } = await getObj(id);
Object.assign(dataForm, data);
dataForm.password = '******';
if (data.roleList) {
dataForm.role = data.roleList.map((item: any) => item.roleId);
}
} finally {
loading.value = false;
}
};
// 角色数据
const getRoleData = () => {
roleList().then((res) => {
roleData.value = res.data;
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,25 @@
export default {
appuser: {
index: '#',
username: 'username',
name: 'name',
phone: 'phone',
post: 'post',
role: 'role',
lockFlag: 'lockFlag',
createTime: 'createTime',
password: 'password',
dept: 'dept',
email: 'email',
avatar: 'avatar',
nickname: 'nickname',
inputNameTip: 'input name',
inputRoleTip: 'input role',
inputUserNameTip: 'input username',
inputPasswordTip: 'input Password',
inputPhoneTip: 'input phone',
inputEmailTip: 'input Email',
inputNickNameTip: 'input NickName',
importUserTip: 'import user',
},
};

View File

@@ -0,0 +1,25 @@
export default {
appuser: {
index: '#',
username: '用户名',
name: '姓名',
phone: '手机号',
post: '岗位',
role: '角色',
lockFlag: '状态',
createTime: '创建时间',
password: '密码',
dept: '部门',
email: '邮箱',
nickname: '昵称',
avatar: '头像',
inputNameTip: '请输入姓名',
inputRoleTip: '请选择角色',
inputUserNameTip: '请输入用户名',
inputPasswordTip: '请输入密码',
inputEmailTip: '请输入邮箱',
inputPhoneTip: '请输入手机号码',
inputNickNameTip: '请输入昵称',
importUserTip: '导入用户',
},
};

View File

@@ -0,0 +1,168 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('appuser.username')" prop="username">
<el-input :placeholder="$t('appuser.inputUserNameTip')" @keyup.enter="getDataList" clearable v-model="state.queryForm.username" />
</el-form-item>
<el-form-item :label="$t('appuser.phone')" prop="phone">
<el-input :placeholder="$t('appuser.inputPhoneTip')" @keyup.enter="getDataList" clearable v-model="state.queryForm.phone" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="Search" type="primary">{{ $t('common.queryBtn') }} </el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="userDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary" v-auth="'app_appuser_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain @click="excelUploadRef.show()" class="ml10" icon="upload-filled" type="primary" v-auth="'app_appuser_export'">
{{ $t('common.importBtn') }}
</el-button>
<el-button plain :disabled="multiple" @click="handleDelete(selectObjs)" class="ml10" icon="Delete" type="primary" v-auth="'app_appuser_del'">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'app_appuser_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
style="width: 100%"
row-key="userId"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="$t('appuser.index')" type="index" width="60" />
<el-table-column :label="$t('appuser.username')" prop="username" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('appuser.nickname')" prop="nickname" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('appuser.avatar')" prop="avatar" show-overflow-tooltip>
<template #default="scope">
<div style="display: flex; justify-content: center">
<ImageUpload v-model:imageUrl="scope.row.avatar" height="50px" width="50px" disabled />
</div>
</template>
</el-table-column>
<el-table-column :label="$t('appuser.role')" show-overflow-tooltip>
<template #default="scope">
<el-tag :key="index" v-for="(item, index) in scope.row.roleList">{{ item.roleName }} </el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('appuser.lockFlag')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="lock_flag" :value="scope.row.lockFlag"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="$t('appuser.createTime')" prop="createTime" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" @click="userDialogRef.openDialog(scope.row.userId)" text type="primary" v-auth="'app_appuser_edit'">
{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" @click="handleDelete([scope.row.userId])" text type="primary" v-auth="'app_appuser_del'">
{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination"> </pagination>
</div>
<user-form @refresh="getDataList()" ref="userDialogRef" />
<upload-excel
:title="$t('appuser.importUserTip')"
@refreshDataList="getDataList"
ref="excelUploadRef"
temp-url="/admin/sys-file/local/file/appuser.xlsx"
url="/admin/appuser/import"
/>
</div>
</template>
<script lang="ts" name="systemUser" setup>
import { delObj, fetchList } from '/@/api/app/appuser';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { useDict } from '/@/hooks/dict';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 动态引入组件
const UserForm = defineAsyncComponent(() => import('./form.vue'));
const ImageUpload = defineAsyncComponent(() => import('/@/components/Upload/Image.vue'));
const { lock_flag } = useDict('lock_flag');
const { t } = useI18n();
// 定义变量内容
const userDialogRef = ref();
const excelUploadRef = ref();
const queryRef = ref();
const showSearch = ref(true);
// 多选rows
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
// 定义表格查询、后台调用的API
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
username: '',
phone: '',
},
pageList: fetchList,
descs: ['create_time'],
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/app/appuser/export',Object.assign( state.queryForm,{ids:selectObjs}), 'users.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { userId: string }[]) => {
selectObjs.value = objs.map(({ userId }) => userId);
multiple.value = !objs.length;
};
// 删除用户
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,80 @@
<template>
<div>
<div>
<draggable class="draggable" v-model="navLists" item-key="index" animation="300">
<template v-slot:item="{ element: item, index }">
<del-wrap class="max-w-[400px]" :key="index" @close="handleDelete(index)">
<div class="flex items-center w-full p-4 mb-4 cursor-move bg-fill-light">
<upload-img v-model:imageUrl="item.image" height="50px" width="50px" iconSize="12" />
<div class="flex-1 ml-3">
<div class="flex">
<span class="flex-none mr-3 text-tx-regular">名称</span>
<el-input v-model="item.name" placeholder="请输入名称" />
</div>
<div class="flex mt-[18px]">
<span class="flex-none mr-3 text-tx-regular">链接</span>
<link-picker v-model="item.link" />
</div>
</div>
</div>
</del-wrap>
</template>
</draggable>
</div>
<div>
<el-button type="primary" @click="handleAdd">添加</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import Draggable from 'vuedraggable';
import { useMessage } from '/@/hooks/message';
const LinkPicker = defineAsyncComponent(() => import('/@/components/Link/picker.vue'));
const props = defineProps({
modelValue: {
type: Array as PropType<any[]>,
default: () => [],
},
max: {
type: Number,
default: 10,
},
min: {
type: Number,
default: 1,
},
});
const emit = defineEmits(['update:modelValue']);
const navLists = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
const handleAdd = () => {
if (props.modelValue?.length < props.max) {
navLists.value.push({
image: '',
name: '导航名称',
link: {},
});
} else {
useMessage().error(`最多添加${props.max}`);
}
};
const handleDelete = (index: number) => {
if (props.modelValue?.length <= props.min) {
return useMessage().error(`最少保留${props.min}`);
}
navLists.value.splice(index, 1);
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,57 @@
<template>
<el-image :style="styles" v-bind="props" :src="src.includes('http') ? src : baseURL + src">
<template #placeholder>
<div class="image-slot"></div>
</template>
<template #error>
<div class="image-slot">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import type { CSSProperties } from 'vue';
import { imageProps } from 'element-plus';
import other from '/@/utils/other';
const props = defineProps({
width: {
type: [String, Number],
default: 'auto',
},
height: {
type: [String, Number],
default: 'auto',
},
radius: {
type: [String, Number],
default: 0,
},
...imageProps,
});
const styles = computed<CSSProperties>(() => {
return {
width: other.addUnit(props.width),
height: other.addUnit(props.height),
borderRadius: other.addUnit(props.radius),
};
});
</script>
<style lang="scss" scoped>
.el-image {
display: block;
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #fafafa;
color: #909399;
}
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<div class="pages-setting">
<div class="title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2">
{{ widget?.title }}
</div>
<keep-alive>
<component class="pt-5 pr-4" :is="widgets[widget?.name]?.attr" :content="widget?.content" :styles="widget?.styles" :type="type" />
</keep-alive>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import widgets from '../widgets';
defineProps({
widget: {
type: Object as PropType<Record<string, any>>,
default: () => ({}),
},
type: {
type: String as PropType<'mobile' | 'pc'>,
default: 'mobile',
},
});
</script>

View File

@@ -0,0 +1,40 @@
<template>
<el-menu :default-active="modelValue" class="w-[160px] min-h-[668px] pages-menu" @select="handleSelect">
<el-menu-item v-for="(item, key) in menus" :index="key" :key="item.id">
<span>{{ item.name }}</span>
</el-menu-item>
</el-menu>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
defineProps({
menus: {
type: Object as PropType<Record<string, any>>,
default: () => ({}),
},
modelValue: {
type: String,
default: '1',
},
});
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void;
}>();
const handleSelect = (index: string) => {
emit('update:modelValue', index);
};
</script>
<style lang="scss" scoped>
.pages-menu {
:deep(.el-menu-item) {
border-color: transparent;
&.is-active {
border-right-width: 2px;
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
}
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<div class="shadow mx-[30px] pages-preview">
<div
v-for="(widget, index) in pageData"
:key="widget.id"
class="relative"
:class="{
'cursor-pointer': !widget?.disabled,
}"
@click="handleClick(widget, index)"
>
<div
class="absolute w-full h-full z-[100] border-dashed"
:class="{
select: index == modelValue,
'border-[#dcdfe6] border-2': !widget?.disabled,
}"
></div>
<slot>
<component :is="widgets[widget?.name]?.content" :content="widget.content" :styles="widget.styles" :key="widget.id" />
</slot>
</div>
<slot name="footer" />
</div>
</template>
<script lang="ts" setup>
import widgets from '../widgets';
import type { PropType } from 'vue';
defineProps({
pageData: {
type: Array as PropType<any[]>,
default: () => [],
},
modelValue: {
type: Number,
default: 0,
},
});
const emit = defineEmits<{
(event: 'update:modelValue', value: number): void;
}>();
const handleClick = (widget: any, index: number) => {
if (widget.disabled) return;
emit('update:modelValue', index);
};
</script>
<style lang="scss" scoped>
.pages-preview {
background-color: #f8f8f8;
width: 360px;
height: 585px;
color: #333;
.select {
@apply border-primary border-solid;
}
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<div>
<el-scrollbar style="height: 550px">
<el-form label-width="70px">
<el-form-item label="是否启用" v-if="type == 'mobile'">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="图片设置">
<div class="flex-1">
<div class="form-tips">最多添加5张建议图片尺寸750px*340px</div>
<draggable class="draggable" v-model="content.data" item-key="index" animation="300">
<template v-slot:item="{ element: item, index }">
<del-wrap :key="index" @close="handleDelete(index)" class="max-w-[640px]">
<div class="flex items-center w-full p-1 mt-4 cursor-move bg-fill-light">
<upload-img v-model:imageUrl="item.image" />
<div class="flex-1 ml-3">
<el-form-item label="图片名称">
<el-input v-model="item.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item class="mt-[18px]" label="图片链接">
<link-picker v-if="type == 'mobile'" v-model="item.link" />
<el-input v-if="type == 'pc'" placeholder="请输入链接" v-model="item.link.path" />
</el-form-item>
</div>
</div>
</del-wrap>
</template>
</draggable>
</div>
</el-form-item>
<el-form-item v-if="content.data?.length < limit">
<el-button type="primary" @click="handleAdd">添加图片</el-button>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import { useMessage } from '/@/hooks/message';
import Draggable from 'vuedraggable';
const LinkPicker = defineAsyncComponent(() => import('/@/components/Link/picker.vue'));
const limit = 5;
type OptionsType = ReturnType<typeof options>;
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
type: {
type: String as PropType<'mobile' | 'pc'>,
default: 'mobile',
},
});
const handleAdd = () => {
if (props.content.data?.length < limit) {
props.content.data.push({
image: '',
name: '',
link: {},
});
} else {
useMessage().error(`最多添加${limit}张图片`);
}
};
const handleDelete = (index: number) => {
if (props.content.data?.length <= 1) {
return useMessage().error('最少保留一张图片');
}
props.content.data.splice(index, 1);
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="banner" :style="styles">
<div class="banner-image w-full h-full">
<decoration-img width="100%" :height="styles.height || height" :src="getImage" fit="contain" />
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import DecorationImg from '../../decoration-img.vue';
type OptionsType = ReturnType<typeof options>;
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
height: {
type: String,
default: '170px',
},
});
const getImage = computed(() => {
const { data } = props.content;
if (Array.isArray(data)) {
return data[0] ? data[0].image : '';
}
return '';
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,15 @@
export default () => ({
title: '首页轮播图',
name: 'banner',
content: {
enabled: 1,
data: [
{
image: '',
name: '',
link: {},
},
],
},
styles: {},
});

View File

@@ -0,0 +1,38 @@
<template>
<div>
<el-form label-width="90px">
<el-form-item label="客服标题">
<el-input class="w-[400px]" v-model="content.title" />
</el-form-item>
<el-form-item label="服务时间">
<el-input class="w-[400px]" v-model="content.time" />
</el-form-item>
<el-form-item label="联系电话">
<el-input class="w-[400px]" v-model="content.mobile" />
</el-form-item>
<el-form-item label="客服二维码">
<div>
<upload-img v-model:imageUrl="content.qrcode" />
<div class="form-tips">建议图片尺寸200*200像素图片格式jpgpngjpeg</div>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped></style>

Some files were not shown because too many files have changed in this diff Show More