mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-29 07:33:58 +00:00
refactor(module): 重构模块路径和数据库配置
- 将前端API路径从`system`和`monitor`重命名为`module_system`和`module_monitor` - 移除MongoDB相关配置和依赖 - 统一数据库类型命名为`mysql`和`postgres` - 修复代码生成模板中的字段命名问题 - 更新依赖项并移除不必要的包 - 优化数据库连接配置 - 添加新的文档和Redoc视图组件 - 修复SQL脚本中的字段注释
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 text-center m-0 mb-20px>{{ t("login.login") }}</h3>
|
||||
<el-form
|
||||
ref="loginFormRef"
|
||||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
size="large"
|
||||
:validate-on-rule-change="false"
|
||||
>
|
||||
<!-- 用户名 -->
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model.trim="loginForm.username" :placeholder="t('login.username')" clearable>
|
||||
<template #prefix>
|
||||
<el-icon><User /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码 -->
|
||||
<el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model.trim="loginForm.password"
|
||||
:placeholder="t('login.password')"
|
||||
type="password"
|
||||
show-password
|
||||
clearable
|
||||
@keyup="checkCapsLock"
|
||||
@keyup.enter="handleLoginSubmit"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<el-form-item v-if="captchaState.enable" prop="captcha">
|
||||
<div flex>
|
||||
<el-input v-model.trim="loginForm.captcha" :placeholder="t('login.captchaCode')" clearable @keyup.enter="handleLoginSubmit" >
|
||||
<template #prefix>
|
||||
<div class="i-svg:captcha" />
|
||||
</template>
|
||||
</el-input>
|
||||
<div cursor-pointer flex-center ml-10px >
|
||||
<el-icon v-if="codeLoading" class="is-loading">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<el-image v-else object-cover :src="captchaState.img_base" @click="getCaptcha" />
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div class="flex-x-between w-full">
|
||||
<el-checkbox v-model="loginForm.remember">{{ t("login.rememberMe") }}</el-checkbox>
|
||||
<el-link type="primary" underline="never" @click="toOtherForm('resetPwd')">
|
||||
{{ t("login.forgetPassword") }}
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<el-form-item>
|
||||
<el-button :loading="loading" type="primary" class="w-full" @click="handleLoginSubmit">
|
||||
{{ t("login.login") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div flex-center gap-10px>
|
||||
<el-text size="default">{{ t("login.noAccount") }}</el-text>
|
||||
<el-link type="primary" underline="never" @click="toOtherForm('register')">
|
||||
{{ t("login.reg") }}
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<div class="third-party-login">
|
||||
<div class="divider-container">
|
||||
<div class="divider-line"></div>
|
||||
<span class="divider-text">{{ t("login.otherLoginMethods") }}</span>
|
||||
<div class="divider-line"></div>
|
||||
</div>
|
||||
<div class="flex-center gap-x-5 w-full text-[var(--el-text-color-secondary)]">
|
||||
<CommonWrapper>
|
||||
<div text-20px class="i-svg:wechat" />
|
||||
</CommonWrapper>
|
||||
<CommonWrapper>
|
||||
<div text-20px cursor-pointer class="i-svg:qq" />
|
||||
</CommonWrapper>
|
||||
<CommonWrapper>
|
||||
<div text-20px cursor-pointer class="i-svg:github" />
|
||||
</CommonWrapper>
|
||||
<CommonWrapper>
|
||||
<div text-20px cursor-pointer class="i-svg:gitee" />
|
||||
</CommonWrapper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from "element-plus";
|
||||
import { LocationQuery, RouteLocationRaw, useRoute, useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { onActivated, onMounted, watch } from "vue";
|
||||
import AuthAPI, {type LoginFormData, type CaptchaInfo } from "@/api/module_system/auth";
|
||||
import { useAppStore, useUserStore, useSettingsStore } from "@/store";
|
||||
import CommonWrapper from "@/components/CommonWrapper/index.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const appStore = useAppStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
// 来自父容器的预填用户名和密码
|
||||
const props = defineProps<{ presetUsername?: string; presetPassword?: string }>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// 组件挂载时获取验证码
|
||||
onMounted(() => getCaptcha());
|
||||
|
||||
// 组件激活时获取验证码(适用于KeepAlive缓存的情况)
|
||||
onActivated(() => {
|
||||
getCaptcha();
|
||||
// 重置登录表单
|
||||
loginForm.captcha = '';
|
||||
});
|
||||
|
||||
// 监听路由变化,确保每次进入登录页面都有最新验证码
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
getCaptcha();
|
||||
loginForm.captcha = '';
|
||||
}
|
||||
);
|
||||
|
||||
const loginFormRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
// 是否大写锁定
|
||||
const isCapsLock = ref(false);
|
||||
|
||||
|
||||
const loginForm = reactive<LoginFormData>({
|
||||
username: "",
|
||||
password: "",
|
||||
captcha: "",
|
||||
captcha_key: "",
|
||||
remember: true,
|
||||
login_type: "PC端",
|
||||
});
|
||||
|
||||
// 监听父组件传入的预填信息,立即填充到登录表单
|
||||
watch(
|
||||
() => [props.presetUsername, props.presetPassword],
|
||||
([presetUsername, presetPassword]) => {
|
||||
if (typeof presetUsername === 'string') {
|
||||
loginForm.username = presetUsername;
|
||||
}
|
||||
if (typeof presetPassword === 'string') {
|
||||
loginForm.password = presetPassword;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const captchaState = reactive<CaptchaInfo>({
|
||||
enable: true,
|
||||
key: "",
|
||||
img_base: ""
|
||||
});
|
||||
|
||||
const loginRules = computed(() => {
|
||||
return {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.username.required"),
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.password.required"),
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: t("login.message.password.min"),
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
captcha: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.captchaCode.required"),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// 获取验证码
|
||||
const codeLoading = ref(false);
|
||||
async function getCaptcha() {
|
||||
try {
|
||||
codeLoading.value = true;
|
||||
const response = await AuthAPI.getCaptcha()
|
||||
loginForm.captcha_key = response.data.data.key;
|
||||
captchaState.img_base = response.data.data.img_base;
|
||||
} finally {
|
||||
codeLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录提交
|
||||
*/
|
||||
async function handleLoginSubmit() {
|
||||
try {
|
||||
// 1. 表单验证
|
||||
const valid = await loginFormRef.value?.validate();
|
||||
if (!valid) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
// 2. 执行登录
|
||||
await userStore.login(loginForm);
|
||||
|
||||
// 3. 登录成功,让路由守卫处理跳转逻辑
|
||||
// 解析目标地址,但不直接跳转
|
||||
const redirect = resolveRedirectTarget(route.query);
|
||||
|
||||
// 通过替换当前路由触发路由守卫,让守卫处理后续的路由生成和跳转
|
||||
await router.replace(redirect);
|
||||
|
||||
// 4. 记住我功能已实现,根据用户选择决定token的存储方式:
|
||||
// - 选中"记住我": token存储在localStorage中,浏览器关闭后仍然有效
|
||||
// - 未选中"记住我": token存储在sessionStorage中,浏览器关闭后失效
|
||||
|
||||
// 登录成功后自动开启项目引导
|
||||
if (settingsStore.showGuide) {
|
||||
appStore.showGuide(true);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
if (error) {
|
||||
getCaptcha(); // 刷新验证码
|
||||
}
|
||||
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析重定向目标
|
||||
*
|
||||
* @param query 路由查询参数
|
||||
* @returns 标准化后的路由地址
|
||||
*/
|
||||
function resolveRedirectTarget(query: LocationQuery): RouteLocationRaw {
|
||||
// 默认跳转路径
|
||||
const defaultPath = "/";
|
||||
|
||||
// 获取原始重定向路径
|
||||
const rawRedirect = (query.redirect as string) || defaultPath;
|
||||
|
||||
try {
|
||||
// 6. 使用Vue Router解析路径
|
||||
const resolved = router.resolve(rawRedirect);
|
||||
return {
|
||||
path: resolved.path,
|
||||
query: resolved.query,
|
||||
};
|
||||
} catch {
|
||||
// 7. 异常处理:返回安全路径
|
||||
return { path: defaultPath };
|
||||
}
|
||||
}
|
||||
|
||||
// 检查输入大小写
|
||||
function checkCapsLock(event: KeyboardEvent) {
|
||||
// 防止浏览器密码自动填充时报错
|
||||
if (event instanceof KeyboardEvent) {
|
||||
isCapsLock.value = event.getModifierState("CapsLock");
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
function toOtherForm(type: "register" | "resetPwd") {
|
||||
emit("update:modelValue", type);
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.third-party-login {
|
||||
.divider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, var(--el-border-color-light), transparent);
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
padding: 0 16px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 text-center m-0 mb-20px>{{ t("login.reg") }}</h3>
|
||||
<el-form ref="formRef" :model="model" :rules="rules" size="large" label-suffix=":" >
|
||||
<!-- 账号 -->
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model.trim="model.username" :placeholder="t('login.username')" clearable>
|
||||
<template #prefix>
|
||||
<el-icon><User /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码 -->
|
||||
<el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model.trim="model.password"
|
||||
:placeholder="t('login.password')"
|
||||
type="password"
|
||||
show-password
|
||||
clearable
|
||||
@keyup="checkCapsLock"
|
||||
@keyup.enter="submit"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input
|
||||
v-model.trim="model.confirmPassword"
|
||||
:placeholder="t('login.message.password.confirm')"
|
||||
type="password"
|
||||
show-password
|
||||
clearable
|
||||
@keyup="checkCapsLock"
|
||||
@keyup.enter="submit"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex-y-center w-full gap-10px">
|
||||
<el-checkbox v-model="isRead">{{ t("login.agree") }}</el-checkbox>
|
||||
<el-link type="primary" underline="never" :href="configStore.configData.sys_web_clause.config_value" target="_blank">{{ t("login.userAgreement") }}</el-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<el-form-item>
|
||||
<el-button :loading="loading" type="success" class="w-full" @click="submit">
|
||||
{{ t("login.register") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div flex-center gap-10px>
|
||||
<el-text size="default">{{ t("login.haveAccount") }}</el-text>
|
||||
<el-link type="primary" underline="never" @click="toLogin">{{ t("login.login") }}</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from "element-plus";
|
||||
import { Lock } from "@element-plus/icons-vue";
|
||||
import UserAPI, { type RegisterForm } from "@/api/module_system/user";
|
||||
import { useConfigStore } from "@/store";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 使用 defineModel 简化具名 v-model 绑定
|
||||
const modelValue = defineModel<string>();
|
||||
const presetUsername = defineModel<string>('presetUsername');
|
||||
const presetPassword = defineModel<string>('presetPassword');
|
||||
const toLogin = () => { modelValue.value = 'login'; };
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const loading = ref(false); // 按钮 loading 状态
|
||||
const isCapsLock = ref(false); // 是否大写锁定
|
||||
const isRead = ref(false);
|
||||
|
||||
|
||||
const model = ref<RegisterForm>({
|
||||
username: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.username.required"),
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.password.required"),
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: t("login.message.password.min"),
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
confirmPassword: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.password.required"),
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: t("login.message.password.min"),
|
||||
trigger: "blur",
|
||||
},
|
||||
{
|
||||
validator: (_: any, value: string) => {
|
||||
return value === model.value.password;
|
||||
},
|
||||
trigger: "blur",
|
||||
message: t("login.message.password.inconformity"),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
// 检查输入大小写
|
||||
function checkCapsLock(event: KeyboardEvent) {
|
||||
// 防止浏览器密码自动填充时报错
|
||||
if (event instanceof KeyboardEvent) {
|
||||
isCapsLock.value = event.getModifierState("CapsLock");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// 0. 检查是否已勾选协议
|
||||
if (!isRead.value) {
|
||||
ElMessage.warning(t("login.message.agree.required"));
|
||||
return;
|
||||
}
|
||||
// 1. 表单验证
|
||||
const valid = await formRef.value?.validate();
|
||||
if (!valid) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
await UserAPI.registerUser(model.value)
|
||||
// 注册成功后,双向绑定回写父容器的用户名和密码,并切回登录
|
||||
presetUsername.value = model.value.username;
|
||||
presetPassword.value = model.value.password;
|
||||
toLogin();
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div>
|
||||
<h3 text-center m-0 mb-20px>{{ t("login.resetPassword") }}</h3>
|
||||
<el-form ref="formRef" :model="model" :rules="rules" size="large" label-suffix=":">
|
||||
<!-- 用户名 -->
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model.trim="model.username" :placeholder="t('login.username')" clearable>
|
||||
<template #prefix>
|
||||
<el-icon><User /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 新密码 -->
|
||||
<el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
|
||||
<el-form-item prop="new_password">
|
||||
<el-input
|
||||
v-model.trim="model.new_password"
|
||||
:placeholder="t('login.password')"
|
||||
type="password"
|
||||
show-password
|
||||
clearable
|
||||
@keyup="checkCapsLock"
|
||||
@keyup.enter="submit"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<el-tooltip :visible="isCapsLock" :content="t('login.capsLock')" placement="right">
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input
|
||||
v-model.trim="model.confirmPassword"
|
||||
:placeholder="t('login.message.password.confirm')"
|
||||
type="password"
|
||||
show-password
|
||||
clearable
|
||||
@keyup="checkCapsLock"
|
||||
@keyup.enter="submit"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Lock /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 重置密码按钮 -->
|
||||
<el-form-item>
|
||||
<el-button type="warning" class="w-full" @click="submit">
|
||||
{{ t("login.resetPassword") }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div flex-center gap-10px>
|
||||
<el-text size="default">{{ t("login.thinkOfPasswd") }}</el-text>
|
||||
<el-link type="primary" underline="never" @click="toLogin">{{ t("login.login") }}</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { FormInstance } from "element-plus";
|
||||
import UserAPI, { type ForgetPasswordForm } from "@/api/module_system/user";
|
||||
|
||||
const loading = ref(false); // 按钮 loading 状态
|
||||
const isCapsLock = ref(false); // 是否大写锁定
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 使用 defineModel 简化具名 v-model 绑定
|
||||
const modelValue = defineModel<string>();
|
||||
const presetUsername = defineModel<string>('presetUsername');
|
||||
const presetPassword = defineModel<string>('presetPassword');
|
||||
const toLogin = () => { modelValue.value = 'login'; };
|
||||
|
||||
const model = ref<ForgetPasswordForm>({
|
||||
username: "",
|
||||
new_password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
const rules = computed(() => {
|
||||
return {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.username.required"),
|
||||
},
|
||||
],
|
||||
new_password: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.password.required"),
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: t("login.message.password.min"),
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
confirmPassword: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.password.required"),
|
||||
},
|
||||
{
|
||||
min: 6,
|
||||
message: t("login.message.password.min"),
|
||||
trigger: "blur",
|
||||
},
|
||||
{
|
||||
validator: (_: any, value: string) => {
|
||||
return value === model.value.new_password;
|
||||
},
|
||||
trigger: "blur",
|
||||
message: t("login.message.password.inconformity"),
|
||||
},
|
||||
],
|
||||
mobile: [
|
||||
{
|
||||
required: true,
|
||||
trigger: "blur",
|
||||
message: t("login.message.captchaCode.required"),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// 1. 表单验证
|
||||
const valid = await formRef.value?.validate();
|
||||
if (!valid) return;
|
||||
|
||||
loading.value = true;
|
||||
|
||||
await UserAPI.forgetPassword(model.value)
|
||||
// 重置成功后,双向绑定回写父容器的用户名和新密码,并切回登录
|
||||
presetUsername.value = model.value.username;
|
||||
presetPassword.value = model.value.new_password;
|
||||
toLogin();
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查输入大小写
|
||||
function checkCapsLock(event: KeyboardEvent) {
|
||||
// 防止浏览器密码自动填充时报错
|
||||
if (event instanceof KeyboardEvent) {
|
||||
isCapsLock.value = event.getModifierState("CapsLock");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="login-container" :style="{ 'background-image': configStore.configData?.sys_login_background?.config_value ? `url(${configStore.configData.sys_login_background.config_value})` : '/background.svg' }">
|
||||
<!-- 右侧切换主题、语言按钮 -->
|
||||
<div class="action-bar">
|
||||
<el-tooltip :content="t('login.themeToggle')" placement="bottom">
|
||||
<CommonWrapper>
|
||||
<DarkModeSwitch />
|
||||
</CommonWrapper>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="t('login.languageToggle')" placement="bottom">
|
||||
<CommonWrapper>
|
||||
<LangSelect size="text-20px" />
|
||||
</CommonWrapper>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<!-- 登录页主体 -->
|
||||
<div flex-1 flex-center>
|
||||
<div
|
||||
class="p-4xl w-full h-auto sm:w-450px border-rd-10px sm:h-680px shadow-[var(--el-box-shadow-light)] backdrop-blur-3px"
|
||||
>
|
||||
<div w-full flex flex-col items-center>
|
||||
<!-- logo -->
|
||||
<!-- <el-image :src="logo" style="width: 84px" /> -->
|
||||
<el-image :src="configStore.configData.sys_web_logo.config_value" style="width: 140px" />
|
||||
|
||||
<!-- 标题 -->
|
||||
<!-- 添加小图标用于显示提示信息 -->
|
||||
<div class="flex items-center justify-center mb-4">
|
||||
<el-tooltip :content="configStore.configData.sys_web_description.config_value" placement="bottom">
|
||||
<el-icon class="cursor-help"><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
<div class="ml-2 text-xl font-bold">
|
||||
<el-badge :value="`v ${configStore.configData.sys_web_version.config_value}`" type="success">
|
||||
{{ configStore.configData.sys_web_title.config_value }}
|
||||
</el-badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组件切换 -->
|
||||
<transition name="fade-slide" mode="out-in">
|
||||
<component
|
||||
:is="formComponents[component]"
|
||||
v-model="component"
|
||||
v-model:preset-username="loginPreset.username"
|
||||
v-model:preset-password="loginPreset.password"
|
||||
class="w-90%"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 登录页底部版权 -->
|
||||
<el-text size="small" class="py-2.5! fixed bottom-0 text-center">
|
||||
<a :href="configStore.configData.sys_git_code.config_value" target="_blank">{{ configStore.configData.sys_web_copyright.config_value }} |</a>
|
||||
<a :href="configStore.configData.sys_help_doc.config_value" target="_blank">帮助 |</a>
|
||||
<a :href="configStore.configData.sys_web_privacy.config_value" target="_blank">隐私 |</a>
|
||||
<a :href="configStore.configData.sys_web_clause.config_value" target="_blank">条款</a>
|
||||
{{ configStore.configData.sys_keep_record.config_value }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// import logo from "@/assets/logo.png";
|
||||
// import { defaultSettings } from "@/settings";
|
||||
import CommonWrapper from "@/components/CommonWrapper/index.vue";
|
||||
import DarkModeSwitch from "@/components/DarkModeSwitch/index.vue";
|
||||
import { useConfigStore } from "@/store";
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
||||
type LayoutMap = "login" | "register" | "resetPwd";
|
||||
|
||||
const t = useI18n().t;
|
||||
|
||||
const component = ref<LayoutMap>("login"); // 切换显示的组件
|
||||
const formComponents = {
|
||||
login: defineAsyncComponent(() => import("./components/Login.vue")),
|
||||
register: defineAsyncComponent(() => import("./components/Register.vue")),
|
||||
resetPwd: defineAsyncComponent(() => import("./components/ResetPwd.vue")),
|
||||
};
|
||||
|
||||
// 预填登录信息(通过具名 v-model 双向绑定传递)
|
||||
const loginPreset = reactive<{ username: string; password: string }>({ username: "admin", password: "123456" });
|
||||
|
||||
onMounted(() => {
|
||||
configStore.getConfig();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 添加伪元素作为背景层
|
||||
.login-container {
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.125rem;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
top: 10px;
|
||||
right: auto;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
top: 40px;
|
||||
right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/* fade-slide */
|
||||
.fade-slide-leave-active,
|
||||
.fade-slide-enter-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,471 @@
|
||||
<!-- 部门配置 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
|
||||
<el-form-item prop="name" label="部门名称">
|
||||
<el-input v-model="queryFormData.name" placeholder="请输入部门名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
|
||||
<el-option value="true" label="启用" />
|
||||
<el-option value="false" label="停用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button v-hasPerm="['system:dept:query']" type="primary" icon="search" native-type="submit">查询</el-button>
|
||||
<el-button v-hasPerm="['system:dept:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="部门管理维护部门信息。">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
部门列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:dept:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:dept:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['system:dept:patch']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
|
||||
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button type="primary" icon="refresh" circle @click="handleRefresh"/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-popover placement="bottom" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="danger" icon="operation" circle />
|
||||
</template>
|
||||
<el-scrollbar max-height="350px">
|
||||
<template v-for="column in tableColumns" :key="column.prop">
|
||||
<el-checkbox v-if="column.prop" v-model="column.show" :label="column.label" />
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-popover>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:系统配置列表 -->
|
||||
<el-table ref="dataTableRef" v-loading="loading" row-key="id" :data="pageTableData" :tree-props="{children: 'children', hasChildren: 'hasChildren'}" class="data-table__content" height="540" border stripe @selection-change="handleSelectionChange" @row-click="handleRowClick">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'index')?.show" type="index" fixed label="序号" min-width="60" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'name')?.show" key="name" label="部门名称" prop="name" min-width="120" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'code')?.show" key="code" label="部门编码" prop="code" min-width="120" />')
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'status')?.show" key="status" label="状态" prop="status" min-width="60">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
|
||||
{{ scope.row.status ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'order')?.show" key="order" label="排序" prop="order" min-width="60" show-overflow-tooltip />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'description')?.show" key="description" label="描述" prop="description" min-width="100" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'created_at')?.show" key="created_at" label="创建时间" prop="created_at" min-width="120" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'updated_at')?.show" key="updated_at" label="更新时间" prop="updated_at" min-width="120" sortable />
|
||||
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-button v-hasPerm="['system:dept:create']" type="success" size="small" link icon="plus" @click="handleOpenDialog('create', undefined, scope.row.id)">新增</el-button>
|
||||
<el-button v-hasPerm="['system:dept:detail']" type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button v-hasPerm="['system:dept:update']" type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
|
||||
<el-button v-hasPerm="['system:dept:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="部门名称" :span="2">{{ detailFormData.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="部门编码" :span="2">{{ detailFormData.code }}</el-descriptions-item>
|
||||
<el-descriptions-item label="上级部门" :span="2">{{ detailFormData.parent_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag :type="detailFormData.status ? 'success' : 'danger'">
|
||||
{{ detailFormData.status ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="排序" :span="2">{{ detailFormData.order }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ detailFormData.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ detailFormData.updated_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="4">{{ detailFormData.description }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
|
||||
<el-form-item label="部门名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入部门名称" :maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入部门编码" :maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上级部门" prop="parent_id">
|
||||
<el-tree-select v-model="formData.parent_id" placeholder="请选择上级部门" :data="deptOptions" filterable check-strictly :render-after-expand="false" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="order">
|
||||
<el-input-number v-model="formData.order" controls-position="right" :min="1" :max="999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="true">启用</el-radio>
|
||||
<el-radio :value="false">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="formData.description" :rows="4" :maxlength="100" show-word-limit type="textarea" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" v-hasPerm="['system:dept:submit']" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else v-hasPerm="['system:dept:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "Dept",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import DeptAPI, { DeptTable, DeptForm, DeptPageQuery } from "@/api/module_system/dept";
|
||||
import { useUserStore } from "@/store";
|
||||
import { formatTree } from "@/utils/common";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<DeptTable[]>([]);
|
||||
|
||||
// 顶级菜单下拉选项
|
||||
const deptOptions = ref<OptionType[]>([]);
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = ref([
|
||||
{ prop: 'selection', label: '选择框', show: true },
|
||||
{ prop: 'index', label: '序号', show: true },
|
||||
{ prop: 'name', label: '部门名称', show: true },
|
||||
{ prop: 'code', label: '部门编码', show: true },
|
||||
{ prop: 'order', label: '排序', show: true },
|
||||
{ prop: 'status', label: '状态', show: true },
|
||||
{ prop: 'description', label: '描述', show: true },
|
||||
{ prop: 'created_at', label: '创建时间', show: true },
|
||||
{ prop: 'updated_at', label: '更新时间', show: true },
|
||||
{ prop: 'operation', label: '操作', show: true }
|
||||
])
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<DeptTable>({});
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<DeptPageQuery>({
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
});
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<DeptForm>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
order: 1,
|
||||
parent_id: undefined,
|
||||
status: true,
|
||||
description: undefined,
|
||||
})
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'create' as 'create' | 'update' | 'detail',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: "请输入部门名称", trigger: "blur" }],
|
||||
order: [{ required: true, message: "请输入排序", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh () {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await DeptAPI.getDeptList(queryFormData);
|
||||
pageTableData.value = response.data.data;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await DeptAPI.getDeptList(queryFormData);
|
||||
pageTableData.value = response.data.data;
|
||||
// 加载部门选项
|
||||
deptOptions.value = formatTree(response.data.data);
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
// 额外清空日期范围与时间查询参数
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: DeptForm = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
order: 1,
|
||||
parent_id: undefined,
|
||||
status: true,
|
||||
description: undefined,
|
||||
}
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
}
|
||||
|
||||
// 选择表格的行菜单ID
|
||||
const selectedDeptId = ref<number | undefined>();
|
||||
|
||||
// 行点击事件
|
||||
async function handleRowClick(row: DeptTable) {
|
||||
selectedDeptId.value = row.id;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开系统配置弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number, parentId?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await DeptAPI.getDeptDetail(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = "部门详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === 'update') {
|
||||
dialogVisible.title = "修改部门";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增部门";
|
||||
formData.id = undefined;
|
||||
// 设置父级部门
|
||||
if (parentId) {
|
||||
formData.parent_id = parentId;
|
||||
}
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 新增、编辑弹窗处理
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
try {
|
||||
if (id) {
|
||||
await DeptAPI.updateDept(id, { id, ...formData })
|
||||
} else {
|
||||
await DeptAPI.createDept(formData)
|
||||
}
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleResetQuery();
|
||||
// 更新全局用户状态,刷新部门信息
|
||||
const userStore = useUserStore();
|
||||
await userStore.getUserInfo();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await DeptAPI.deleteDept(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: boolean) {
|
||||
if (selectIds.value.length) {
|
||||
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await DeptAPI.batchAvailableDept({ ids: selectIds.value, status });
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,600 @@
|
||||
<!-- 字典数据 -->
|
||||
<template>
|
||||
<el-drawer v-model="drawerVisible" :title="'【' + props.dictLabel + '】字典数据'" :size="drawerSize">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
|
||||
|
||||
<el-form-item prop="dict_label" label="字典数据标签">
|
||||
<el-input v-model="queryFormData.dict_label" placeholder="请输入字典标签" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
|
||||
<el-option value="true" label="启用" />
|
||||
<el-option value="false" label="停用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.creator"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" icon="search" native-type="submit">查询</el-button>
|
||||
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="字典数据管理维护系统的字典数据。">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
字典数据列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:dict_data:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:dict_data:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['system:dict_data:patch']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
|
||||
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button v-hasPerm="['system:dict_data:export']" type="warning" icon="download" circle @click="handleOpenExportsModal"/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button type="default" icon="refresh" circle @click="handleRefresh" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:系统配置列表 -->
|
||||
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="460" border stripe @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column type="index" fixed label="序号" min-width="60">
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标签" prop="dict_label" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="状态" prop="status" min-width="100" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
|
||||
{{ scope.row.status ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" prop="dict_type" min-width="180" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-tag type="primary">{{ scope.row.dict_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="值" prop="dict_value" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="样式属性" prop="css_class" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="列表类样式" prop="list_class" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="排序" prop="dict_sort" min-width="60" />
|
||||
<el-table-column label="是否默认" prop="is_default" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.is_default" type="success">是</el-tag>
|
||||
<el-tag v-else type="danger">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="描述" prop="description" min-width="100" show-overflow-tooltip/>
|
||||
<el-table-column label="创建时间" prop="created_at" min-width="200" sortable />
|
||||
<el-table-column label="更新时间" prop="updated_at" min-width="200" sortable />
|
||||
<el-table-column label="创建人" prop="creator" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.creator?.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" align="center" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button v-hasPerm="['system:dict_data:update']" type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
|
||||
<el-button v-hasPerm="['system:dict_data:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="数据标签" :span="2">{{ detailFormData.dict_label }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数据类型" :span="2">{{ detailFormData.dict_type }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数据值" :span="2">{{ detailFormData.dict_value }}</el-descriptions-item>
|
||||
<el-descriptions-item label="样式属性" :span="2">{{ detailFormData.css_class }}</el-descriptions-item>
|
||||
<el-descriptions-item label="列表样式属性" :span="2">{{ detailFormData.list_class }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否默认" :span="2">
|
||||
<el-tag v-if="detailFormData.is_default" type="success">是</el-tag>
|
||||
<el-tag v-else type="danger">否</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag v-if="detailFormData.status" type="success">启用</el-tag>
|
||||
<el-tag v-else type="danger">停用</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="排序" :span="2">{{ detailFormData.dict_sort }}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">{{ detailFormData.description }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人" :span="2">{{ detailFormData.creator?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ detailFormData.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ detailFormData.updated_at }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
|
||||
<el-form-item label="数据类型" prop="dict_type">
|
||||
<el-input v-model="formData.dict_type" placeholder="请输入数据类型" :maxlength="50" :disabled="true" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据标签" prop="dict_label">
|
||||
<el-input v-model="formData.dict_label" placeholder="请输入数据标签" :maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据值" prop="dict_value">
|
||||
<el-input v-model="formData.dict_value" placeholder="请输入数据值" :maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="样式属性" prop="css_class">
|
||||
<el-select v-model="formData.css_class" placeholder="请选择常用颜色或输入自定义" clearable filterable allow-create default-first-option>
|
||||
<!-- Element Plus 常用主题色(十六进制) -->
|
||||
<el-option value="#409EFF" label="蓝色-Primary (#409EFF)" />
|
||||
<el-option value="#67C23A" label="绿色-Success (#67C23A)" />
|
||||
<el-option value="#E6A23C" label="橙色-Warning (#E6A23C)" />
|
||||
<el-option value="#F56C6C" label="红色-Danger (#F56C6C)" />
|
||||
<el-option value="#909399" label="灰色-Info (#909399)" />
|
||||
<!-- 文本常用颜色 -->
|
||||
<el-option value="#303133" label="深灰-文本主要 (#303133)" />
|
||||
<el-option value="#606266" label="灰色-文本常规 (#606266)" />
|
||||
<el-option value="#C0C4CC" label="浅灰-占位文本 (#C0C4CC)" />
|
||||
<!-- 基础色 -->
|
||||
<el-option value="#000000" label="黑色 (#000000)" />
|
||||
<el-option value="#FFFFFF" label="白色 (#FFFFFF)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="列表类样式" prop="list_class">
|
||||
<el-select v-model="formData.list_class" placeholder="请选择列表类样式" clearable>
|
||||
<el-option value="default" label="默认(default)" />
|
||||
<el-option value="primary" label="主要(primary)" />
|
||||
<el-option value="success" label="成功(success)" />
|
||||
<el-option value="warning" label="警告(warning)" />
|
||||
<el-option value="danger" label="危险(danger)" />
|
||||
<el-option value="info" label="信息(info)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否默认" prop="is_default">
|
||||
<el-radio-group v-model="formData.is_default">
|
||||
<el-radio :value="true">是</el-radio>
|
||||
<el-radio :value="false">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="dict_sort">
|
||||
<el-input-number v-model="formData.dict_sort" controls-position="right" :min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch v-model="formData.status" inline-prompt :active-value="true" :inactive-value="false" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="formData.description" :rows="4" :maxlength="100" show-word-limit type="textarea" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</el-drawer>
|
||||
<ExportModal
|
||||
v-model="exportsDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
:query-params="queryFormData"
|
||||
:page-data="pageTableData"
|
||||
:selection-data="selectionRows"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 添加 props 来接收 dictType
|
||||
const props = defineProps({
|
||||
dictType: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
dictLabel: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const drawerVisible = defineModel<boolean>()
|
||||
import DictAPI, { DictDataTable, DictDataForm, DictDataPageQuery } from "@/api/module_system/dict";
|
||||
import { useAppStore } from "@/store/modules/app.store";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "80%" : "60%"));
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<DictDataTable[]>([]);
|
||||
|
||||
// 导出弹窗显示状态 & 选中行
|
||||
const exportsDialogVisible = ref(false);
|
||||
const selectionRows = ref<DictDataTable[]>([]);
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<DictDataTable>({});
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<DictDataPageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
dict_label: undefined,
|
||||
dict_type: props.dictType,
|
||||
status: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
// 创建人
|
||||
creator: undefined,
|
||||
});
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<DictDataForm>({
|
||||
id: undefined,
|
||||
dict_sort: 1,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
dict_type: '',
|
||||
css_class: '',
|
||||
list_class: undefined,
|
||||
is_default: false,
|
||||
status: true,
|
||||
description: ''
|
||||
})
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'create' as 'create' | 'update' | 'detail',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
dict_label: [{ required: true, message: "请输入字典标签", trigger: "blur" }],
|
||||
dict_type: [{ required: true, message: "请输入字典类型", trigger: "blur" }],
|
||||
dict_value: [{ required: true, message: "请输入字典键值", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
|
||||
dict_sort: [{ required: true, message: "请输入排序", trigger: "blur" }],
|
||||
// css_class: [{ required: true, message: "请输入样式类名", trigger: "blur" }],
|
||||
// list_class: [{ required: true, message: "请输入列表类名", trigger: "blur" }],
|
||||
is_default: [{ required: true, message: "请选择是否默认", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh () {
|
||||
await loadingData();
|
||||
};
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 在查询参数中添加 dictType
|
||||
const response = await DictAPI.getDictDataList(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
// 额外清空日期范围与时间查询参数
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: DictDataForm = {
|
||||
id: undefined,
|
||||
dict_sort: 1,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
dict_type: '',
|
||||
css_class: '',
|
||||
list_class: undefined,
|
||||
is_default: false,
|
||||
status: true,
|
||||
description: ''
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开系统配置弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await DictAPI.getDictDataDetail(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = "字典数据详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === 'update') {
|
||||
dialogVisible.title = "修改字典数据";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增字典数据";
|
||||
// 重置为初始值并确保状态默认开启
|
||||
Object.assign(formData, initialFormData);
|
||||
formData.dict_type = props.dictType;
|
||||
formData.status = true;
|
||||
formData.id = undefined;
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 新增、编辑弹窗处理
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
if (id) {
|
||||
try {
|
||||
await DictAPI.updateDictData(id, { id, ...formData })
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await DictAPI.createDictData(formData)
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await DictAPI.deleteDictData(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 打开导出弹窗
|
||||
function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: boolean) {
|
||||
if (selectIds.value.length) {
|
||||
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await DictAPI.batchAvailableDictData({ ids: selectIds.value, status });
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出字段
|
||||
const exportColumns = [
|
||||
{ prop: 'dict_label', label: '数据标签' },
|
||||
{ prop: 'dict_type', label: '数据类型' },
|
||||
{ prop: 'dict_value', label: '数据值' },
|
||||
{ prop: 'css_class', label: '样式属性' },
|
||||
{ prop: 'list_class', label: '列表类样式' },
|
||||
{ prop: 'dict_sort', label: '排序' },
|
||||
{ prop: 'is_default', label: '是否默认' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'description', label: '描述' },
|
||||
{ prop: 'created_at', label: '创建时间' },
|
||||
{ prop: 'updated_at', label: '更新时间' },
|
||||
];
|
||||
|
||||
// 导出配置(用于导出弹窗)
|
||||
const curdContentConfig = {
|
||||
permPrefix: 'system:dict_data',
|
||||
cols: exportColumns as any,
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
if (typeof query.status === 'string') query.status = query.status === 'true';
|
||||
// dict_type 已在查询表单中设置为 props.dictType
|
||||
query.page_no = 1;
|
||||
query.page_size = 1000;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await DictAPI.getDictDataList(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
onMounted(() => {
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,559 @@
|
||||
<!-- 字典 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
|
||||
<el-form-item prop="dict_name" label="字典名称">
|
||||
<el-input v-model="queryFormData.dict_name" placeholder="请输入字典名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="dict_type" label="字典类型">
|
||||
<el-input v-model="queryFormData.dict_type" placeholder="请输入字典类型" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
|
||||
<el-option value="true" label="启用" />
|
||||
<el-option value="false" label="停用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.creator"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button v-hasPerm="['system:dict_type:query']" type="primary" icon="search" native-type="submit">查询</el-button>
|
||||
<el-button v-hasPerm="['system:dict_type:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="字典管理维护系统的字典。">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
字典列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:dict_type:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:dict_type:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['system:dict_type:patch']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">
|
||||
更多
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
|
||||
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button v-hasPerm="['system:dict_type:export']" type="warning" icon="download" circle @click="handleOpenExportsModal"/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button v-hasPerm="['system:dict_type:refresh']" type="primary" icon="refresh" circle @click="handleRefresh"/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-popover placement="bottom" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="danger" icon="operation" circle></el-button>
|
||||
</template>
|
||||
<el-scrollbar max-height="350px">
|
||||
<template v-for="column in tableColumns" :key="column.prop">
|
||||
<el-checkbox v-if="column.prop" v-model="column.show" :label="column.label" />
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-popover>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:系统配置列表 -->
|
||||
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'index')?.show" type="index" fixed label="序号" min-width="60">
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'dict_name')?.show" key="dict_name" label="字典名称" prop="dict_name" min-width="140" show-overflow-tooltip/>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'dict_type')?.show" key="dict_type" label="字典类型" prop="dict_type" min-width="180">
|
||||
<template #default="scope">
|
||||
<el-tag type="primary">{{ scope.row.dict_type }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'status')?.show" key="status" label="状态" prop="status" min-width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
|
||||
{{ scope.row.status ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'description')?.show" key="description" label="描述" prop="description" min-width="140" show-overflow-tooltip/>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'created_at')?.show" key="created_at" label="创建时间" prop="created_at" min-width="200" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'updated_at')?.show" key="updated_at" label="更新时间" prop="updated_at" min-width="200" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'creator')?.show" label="创建人" prop="creator" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.creator?.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="260">
|
||||
<template #default="scope">
|
||||
<el-button v-hasPerm="['system:dict_data:query']" type="warning" size="small" link icon="document" @click="handleDictDataDrawer(scope.row.dict_type, scope.row.dict_name)">字典</el-button>
|
||||
<el-button v-hasPerm="['system:dict_type:detail']" type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button v-hasPerm="['system:dict_type:update']" type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
|
||||
<el-button v-hasPerm="['system:dict_type:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="字典名称" :span="2">{{ detailFormData.dict_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="字典类型" :span="2">
|
||||
<el-tag type="primary">{{ detailFormData.dict_type }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag v-if="detailFormData.status" type="success">启用</el-tag>
|
||||
<el-tag v-else type="danger">停用</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">{{ detailFormData.description }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人" :span="2">{{ detailFormData.creator?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ detailFormData.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ detailFormData.updated_at }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
|
||||
<el-form-item label="字典名称" prop="dict_name">
|
||||
<el-input v-model="formData.dict_name" placeholder="请输入字典名称" :maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典类型" prop="dict_type">
|
||||
<el-input v-model="formData.dict_type" placeholder="请输入字典类型" :maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="true">启用</el-radio>
|
||||
<el-radio :value="false">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="formData.description" :rows="4" :maxlength="100" show-word-limit type="textarea" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" v-hasPerm="['system:dict_type:submit']" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else v-hasPerm="['system:dict_type:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<DataDrawer v-if="drawerVisible" v-model="drawerVisible" :dict-type="currentDictType" :dict-label="currentDictLabel" />
|
||||
<ExportModal
|
||||
v-model="exportsDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
:query-params="queryFormData"
|
||||
:page-data="pageTableData"
|
||||
:selection-data="selectionRows"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "Dict",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import DictAPI, { DictTable, DictForm, DictPageQuery } from "@/api/module_system/dict";
|
||||
import DataDrawer from "@/views/module_system/dict/components/DataDrawer.vue"
|
||||
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<DictTable[]>([]);
|
||||
|
||||
// 导出弹窗显示状态 & 选中行
|
||||
const exportsDialogVisible = ref(false);
|
||||
const selectionRows = ref<DictTable[]>([]);
|
||||
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = ref([
|
||||
{ prop: 'selection', label: '选择框', show: true },
|
||||
{ prop: 'index', label: '序号', show: true },
|
||||
{ prop: 'dict_name', label: '字典名称', show: true },
|
||||
{ prop: 'dict_type', label: '字典类型', show: true },
|
||||
{ prop: 'status', label: '状态', show: true },
|
||||
{ prop: 'description', label: '描述', show: true },
|
||||
{ prop: 'creator', label: '创建人', show: true },
|
||||
{ prop: 'created_at', label: '创建时间', show: true },
|
||||
{ prop: 'updated_at', label: '更新时间', show: true },
|
||||
{ prop: 'operation', label: '操作', show: true }
|
||||
])
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<DictTable>({});
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<DictPageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
dict_name: undefined,
|
||||
dict_type: undefined,
|
||||
status: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
// 创建人
|
||||
creator: undefined,
|
||||
});
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<DictForm>({
|
||||
id: undefined,
|
||||
dict_name: '',
|
||||
dict_type: '',
|
||||
status: true,
|
||||
description: undefined,
|
||||
})
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'create' as 'create' | 'update' | 'detail',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
dict_name: [{ required: true, message: "请输入字典名称", trigger: "blur" }],
|
||||
dict_type: [{ required: true, message: "请选择字典类型", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择字典状态", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh () {
|
||||
await loadingData();
|
||||
};
|
||||
|
||||
|
||||
// 抽屉显隐
|
||||
const drawerVisible = ref(false);
|
||||
|
||||
// 添加字典类型变量
|
||||
const currentDictType = ref('');
|
||||
|
||||
// 添加字典名称变量
|
||||
const currentDictLabel = ref('');
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await DictAPI.getDictTypeList(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
// 额外清空日期范围与时间查询参数
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: DictForm = {
|
||||
id: undefined,
|
||||
dict_name: '',
|
||||
dict_type: '',
|
||||
status: true,
|
||||
description: undefined,
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开系统配置弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await DictAPI.getDictTypeDetail(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = "字典详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === 'update') {
|
||||
dialogVisible.title = "修改字典";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增字典";
|
||||
formData.id = undefined;
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 新增、编辑弹窗处理
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
if (id) {
|
||||
try {
|
||||
await DictAPI.updateDictType(id, { id, ...formData })
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await DictAPI.createDictType(formData)
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await DictAPI.deleteDictType(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 打开导出弹窗
|
||||
function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: boolean) {
|
||||
if (selectIds.value.length) {
|
||||
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await DictAPI.batchAvailableDict({ ids: selectIds.value, status });
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleDictDataDrawer(dictType: string, dictLabel: string) {
|
||||
currentDictType.value = dictType;
|
||||
currentDictLabel.value = dictLabel;
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
// 导出字段
|
||||
const exportColumns = [
|
||||
{ prop: 'dict_name', label: '字典名称' },
|
||||
{ prop: 'dict_type', label: '字典类型' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'description', label: '描述' },
|
||||
{ prop: 'created_at', label: '创建时间' },
|
||||
{ prop: 'updated_at', label: '更新时间' },
|
||||
];
|
||||
|
||||
// 导出配置(用于导出弹窗)
|
||||
const curdContentConfig = {
|
||||
permPrefix: 'system:dict_type',
|
||||
cols: exportColumns as any,
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
if (typeof query.status === 'string') query.status = query.status === 'true';
|
||||
query.page_no = 1;
|
||||
query.page_size = 1000;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await DictAPI.getDictTypeList(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
onMounted(() => {
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,457 @@
|
||||
<!-- 日志管理 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
|
||||
<el-form-item prop="request_path" label="请求路径">
|
||||
<el-input v-model="queryFormData.request_path" placeholder="请输入请求路径" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="type" label="日志类型">
|
||||
<el-select v-model="queryFormData.type" placeholder="请选择日志类型" style="width: 167.5px" clearable>
|
||||
<el-option label="登录日志" value=1 />
|
||||
<el-option label="操作日志" value=2 />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.creator"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button v-hasPerm="['system:log:query']" type="primary" icon="search" native-type="submit">查询</el-button>
|
||||
<el-button v-hasPerm="['system:log:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="日志管理维护系统的日志。">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
日志列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:log:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button v-hasPerm="['system:log:export']" type="warning" icon="download" circle @click="handleOpenExportsModal"/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button v-hasPerm="['system:log:refresh']" type="default" icon="refresh" circle @click="handleRefresh"/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:系统配置列表 -->
|
||||
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column prop='selection' type="selection" min-width="55" align="center" />
|
||||
<el-table-column type="index" fixed label="序号" min-width="60" >
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="日志类型" prop="type" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.type === 1 ? 'success' : 'primary'">
|
||||
{{ scope.row.type === 1 ? '登录日志' : '操作日志' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="请求路径" prop="request_path" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="请求方法" prop="request_method" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getMethodType(scope.row.request_method)">
|
||||
{{ scope.row.request_method }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态码" prop="response_code" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusCodeType(scope.row.response_code)">
|
||||
{{ scope.row.response_code }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="请求IP" prop="request_ip" min-width="180" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-text>{{ scope.row.request_ip }}</el-text>
|
||||
<CopyButton v-if="scope.row.request_ip" :text="scope.row.request_ip" style="margin-left: 2px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理时间" prop="process_time" min-width="120" />
|
||||
<el-table-column label="浏览器" prop="request_browser" min-width="220" show-overflow-tooltip/>
|
||||
<el-table-column label="系统" prop="request_os" min-width="100" />
|
||||
<el-table-column label="描述" prop="description" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" prop="created_at" min-width="200" sortable />
|
||||
<el-table-column label="创建人" prop="creator" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.creator?.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" align="center" min-width="150">
|
||||
<template #default="scope">
|
||||
<el-button v-hasPerm="['system:log:detail']" type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button v-hasPerm="['system:log:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="8" border label-width="200px">
|
||||
<el-descriptions-item label="日志类型" :span="2">
|
||||
<el-tag :type="formData.type === 1 ? 'success' : 'primary'">
|
||||
{{ formData.type === 1 ? '登录日志' : '操作日志' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求路径" :span="2">{{ formData.request_path }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求方法" :span="2">
|
||||
<el-tag :type="getMethodType(formData.request_method)">
|
||||
{{ formData.request_method }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="响应状态码" :span="2">
|
||||
<el-tag :type="getStatusCodeType(formData.response_code)">
|
||||
{{ formData.response_code }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求IP" :span="2">{{ formData.request_ip }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理时间" :span="2">{{ formData.process_time }}</el-descriptions-item>
|
||||
<el-descriptions-item label="浏览器" :span="2">{{ formData.request_browser }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作系统" :span="2">{{ formData.request_os }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求参数" :span="8">
|
||||
<JsonPretty :value="formData.request_payload" height="80px" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="响应数据" :span="8">
|
||||
<JsonPretty :value="formData.response_json" height="140px" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="登录地点" :span="4">{{ formData.login_location }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人" :span="4">{{ formData.creator?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="4">{{ formData.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="4">{{ formData.updated_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="8">{{ formData.description }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-hasPerm="['system:log:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 导出弹窗 -->
|
||||
<ExportModal
|
||||
v-model="exportsDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
:query-params="queryFormData"
|
||||
:page-data="pageTableData"
|
||||
:selection-data="selectionRows"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "Log",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import LogAPI, { LogTable, LogPageQuery } from "@/api/module_system/log";
|
||||
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import JsonPretty from "@/components/JsonPretty/index.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<LogTable[]>([]);
|
||||
|
||||
// 导出弹窗状态和选中行数据
|
||||
const exportsDialogVisible = ref(false);
|
||||
const selectionRows = ref<LogTable[]>([]);
|
||||
|
||||
// 详情表单
|
||||
const formData = ref<LogTable>({});
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<LogPageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
type: undefined,
|
||||
request_path: undefined,
|
||||
creator_name: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
// 创建人
|
||||
creator: undefined,
|
||||
});
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'create' as 'create' | 'update' | 'detail',
|
||||
});
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh () {
|
||||
await loadingData();
|
||||
};
|
||||
|
||||
const getStatusCodeType = (code?: number) => {
|
||||
if (code === undefined) {
|
||||
return 'info';
|
||||
}
|
||||
if (code >= 200 && code < 300) {
|
||||
return 'success';
|
||||
} else if (code >= 300 && code < 400) {
|
||||
return 'warning';
|
||||
} else if (code >= 400 && code < 500) {
|
||||
return 'danger';
|
||||
} else {
|
||||
return 'danger';
|
||||
}
|
||||
}
|
||||
|
||||
const getMethodType = (method?: string) => {
|
||||
if (method === undefined) {
|
||||
return 'info';
|
||||
}
|
||||
if (method === 'GET') {
|
||||
return 'info';
|
||||
} else if (method === 'POST') {
|
||||
return 'success';
|
||||
} else if (method === 'PUT' || method === 'PATCH') {
|
||||
return 'warning';
|
||||
} else if (method === 'DELETE') {
|
||||
return 'danger';
|
||||
} else {
|
||||
return 'info';
|
||||
}
|
||||
}
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await LogAPI.getLogList(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
// 额外清空日期范围与时间查询参数
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
formData.value.id = undefined;
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开日志详情弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await LogAPI.getLogDetail(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = "日志详情";
|
||||
Object.assign(formData.value, response.data.data);
|
||||
}
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await LogAPI.deleteLog(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 打开导出弹窗
|
||||
function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 导出字段
|
||||
const exportColumns = [
|
||||
{ prop: 'type', label: '日志类型' },
|
||||
{ prop: 'request_path', label: '请求路径' },
|
||||
{ prop: 'request_method', label: '请求方法' },
|
||||
{ prop: 'response_code', label: '状态码' },
|
||||
{ prop: 'request_ip', label: '请求IP' },
|
||||
{ prop: 'login_location', label: '登录地点' },
|
||||
{ prop: 'process_time', label: '处理时间' },
|
||||
{ prop: 'request_browser', label: '浏览器' },
|
||||
{ prop: 'request_os', label: '系统' },
|
||||
{ prop: 'description', label: '描述' },
|
||||
{ prop: 'created_at', label: '创建时间' },
|
||||
{ prop: 'updated_at', label: '更新时间' },
|
||||
];
|
||||
|
||||
// 导入/导出配置(用于导出弹窗)
|
||||
const curdContentConfig = {
|
||||
permPrefix: 'system:log',
|
||||
cols: exportColumns as any,
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
query.page_no = 1;
|
||||
query.page_size = 1000;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await LogAPI.getLogList(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
onMounted(() => {
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,814 @@
|
||||
<!-- 菜单管理 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
|
||||
<el-form-item prop="name" label="菜单名称">
|
||||
<el-input v-model="queryFormData.name" placeholder="请输入菜单名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
|
||||
<el-option value="true" label="启用" />
|
||||
<el-option value="false" label="停用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button v-hasPerm="['system:menu:query']" type="primary" icon="search" native-type="submit">查询</el-button>
|
||||
<el-button v-hasPerm="['system:menu:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="菜单管理系统的菜单和权限。">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
菜单列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:menu:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:menu:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['system:menu:patch']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
|
||||
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button v-hasPerm="['system:menu:refresh']" type="primary" icon="refresh" circle @click="handleRefresh"/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<el-table ref="dataTableRef" v-loading="loading" row-key="id" :data="pageTableData" :tree-props="{children: 'children',hasChildren: 'hasChildren'}" class="data-table__content" height="540" border stripe @selection-change="handleSelectionChange" @row-click="handleRowClick">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column type="index" fixed label="序号" min-width="60" />
|
||||
<el-table-column label="菜单名称" prop="name" min-width="240" />
|
||||
<el-table-column label="图标" prop="icon" min-width="80" align="center">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.icon && scope.row.icon.startsWith('el-icon')">
|
||||
<el-icon style="vertical-align: -0.15em">
|
||||
<component :is="scope.row.icon.replace('el-icon-', '')" />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template v-else-if="scope.row.icon">
|
||||
<div :class="`i-svg:${scope.row.icon}`" />
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" min-width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
|
||||
{{ scope.row.status ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" prop="type" min-width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.type === MenuTypeEnum.CATALOG" type="warning">目录</el-tag>
|
||||
<el-tag v-if="scope.row.type === MenuTypeEnum.MENU" type="success">菜单</el-tag>
|
||||
<el-tag v-if="scope.row.type === MenuTypeEnum.BUTTON" type="danger">按钮</el-tag>
|
||||
<el-tag v-if="scope.row.type === MenuTypeEnum.EXTLINK" type="info">外链</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="order" min-width="80" />
|
||||
<el-table-column label="重定向" prop="redirect" min-width="200" />
|
||||
<el-table-column label="是否缓存" prop="keep_alive" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.keep_alive ? 'success' : 'danger'">
|
||||
{{ scope.row.keep_alive ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否隐藏" prop="hidden" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.hidden ? 'success' : 'danger'">
|
||||
{{ scope.row.hidden ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="显示根路由" prop="always_show" min-width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.always_show ? 'success' : 'danger'">
|
||||
{{ scope.row.always_show ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="固定路由" prop="affix" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.affix ? 'success' : 'danger'">
|
||||
{{ scope.row.affix ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="菜单标题" prop="title" min-width="200"/>
|
||||
<el-table-column label="权限标识" prop="permission" min-width="220" />
|
||||
<el-table-column label="路由名称" prop="route_name" min-width="200" />
|
||||
<el-table-column label="路由路径" prop="route_path" min-width="200" />
|
||||
<el-table-column label="组件路径" prop="component_path" min-width="200" />
|
||||
<el-table-column label="路由参数" prop="params" min-width="100" />
|
||||
<el-table-column label="描述" prop="description" min-width="200" />
|
||||
<el-table-column label="创建时间" prop="created_at" min-width="200" sortable />
|
||||
<el-table-column label="更新时间" prop="updated_at" min-width="200" sortable />
|
||||
|
||||
<el-table-column fixed="right" label="操作" align="center" min-width="260">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.type == MenuTypeEnum.CATALOG || scope.row.type == MenuTypeEnum.MENU" v-hasPerm="['system:menu:create']" type="success" link size="small" icon="plus" @click.stop="handleOpenDialog('create', undefined, scope.row.id)">新增</el-button>
|
||||
<el-button v-hasPerm="['system:menu:detail']" type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button v-hasPerm="['system:menu:update']" type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
|
||||
<el-button v-hasPerm="['system:menu:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-drawer v-model="dialogVisible.visible" :title="dialogVisible.title" :size="drawerSize" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="编号" :span="2">{{ detailFormData.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="菜单名称" :span="2">{{ detailFormData.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="菜单类型" :span="2">
|
||||
<el-tag v-if="detailFormData.type === MenuTypeEnum.CATALOG" type="warning">目录</el-tag>
|
||||
<el-tag v-if="detailFormData.type === MenuTypeEnum.MENU" type="success">菜单</el-tag>
|
||||
<el-tag v-if="detailFormData.type === MenuTypeEnum.BUTTON" type="danger">按钮</el-tag>
|
||||
<el-tag v-if="detailFormData.type === MenuTypeEnum.EXTLINK" type="info">外链</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="图标" :span="2">
|
||||
<template #default>
|
||||
<template v-if=" detailFormData.icon && detailFormData.icon.startsWith('el-icon')">
|
||||
<el-icon style="vertical-align: -0.15em">
|
||||
<component :is="detailFormData.icon.replace('el-icon-', '')" />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template v-else-if=" detailFormData.icon">
|
||||
<div :class="`i-svg:${detailFormData.icon}`" />
|
||||
</template>
|
||||
</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="排序" :span="2">{{ detailFormData.order }}</el-descriptions-item>
|
||||
<el-descriptions-item label="权限标识" :span="2">{{ detailFormData.permission }}</el-descriptions-item>
|
||||
<el-descriptions-item label="路由名称" :span="2">{{ detailFormData.route_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="路由路径" :span="2">{{ detailFormData.route_path }}</el-descriptions-item>
|
||||
<el-descriptions-item label="组件路径" :span="2">{{ detailFormData.component_path }}</el-descriptions-item>
|
||||
<el-descriptions-item label="重定向" :span="2">{{ detailFormData.redirect }}</el-descriptions-item>
|
||||
<el-descriptions-item label="父级编号" :span="2">{{ detailFormData.parent_id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="父级菜单" :span="2">{{ detailFormData.parent_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否缓存" :span="2">
|
||||
<el-tag :type="detailFormData.keep_alive ? 'success' : 'danger'">
|
||||
{{ detailFormData.keep_alive ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="是否显示" :span="2">
|
||||
<el-tag :type="detailFormData.hidden ? 'success' : 'danger'">
|
||||
{{ detailFormData.hidden ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="是否显示根路由" :span="2">
|
||||
<el-tag :type="detailFormData.always_show ? 'success' : 'danger'">
|
||||
{{ detailFormData.always_show ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="菜单标题" :span="2">{{ detailFormData.title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="路由参数" :span="2">{{ detailFormData.params }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否固定路由" :span="2">
|
||||
<el-tag :type="detailFormData.affix ? 'success' : 'danger'">
|
||||
{{ detailFormData.affix ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag :type="detailFormData.status ? 'success' : 'danger'">
|
||||
{{ detailFormData.status ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="排序" :span="2">{{ detailFormData.order }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ detailFormData.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ detailFormData.updated_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="4">{{ detailFormData.description }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
|
||||
<el-form-item v-if="formData.type !== MenuTypeEnum.CATALOG" label="父级菜单" prop="parent_id" if >
|
||||
<el-tree-select v-model="formData.parent_id" placeholder="选择上级菜单" :data="menuOptions" filterable check-strictly :render-after-expand="false" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单标题" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入菜单标题" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单类型" prop="type">
|
||||
<el-radio-group v-model="formData.type" @change="handleMenuTypeChange">
|
||||
<el-radio :value="MenuTypeEnum.CATALOG">目录</el-radio>
|
||||
<el-radio :value="MenuTypeEnum.MENU">菜单</el-radio>
|
||||
<el-radio :value="MenuTypeEnum.BUTTON">按钮</el-radio>
|
||||
<el-radio :value="MenuTypeEnum.EXTLINK">外链</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.EXTLINK" label="外链地址" prop="path">
|
||||
<el-input v-model="formData.route_path" placeholder="请输入外链完整路径" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="route_name">
|
||||
<template #label>
|
||||
<div class="flex-y-center">
|
||||
路由名称
|
||||
<el-tooltip placement="bottom" effect="light">
|
||||
<template #content>
|
||||
如果需要开启缓存,需保证页面 defineOptions 中的 name 与此处一致,建议使用驼峰。
|
||||
</template>
|
||||
<el-icon class="ml-1 cursor-pointer">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="formData.route_name" placeholder="请输入路由名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.CATALOG || formData.type == MenuTypeEnum.MENU" prop="route_path">
|
||||
<template #label>
|
||||
<div class="flex-y-center">
|
||||
路由路径
|
||||
<el-tooltip placement="bottom" effect="light">
|
||||
<template #content>
|
||||
定义应用中不同页面对应的 URL 路径,目录需以 / 开头,菜单项不用。例如:系统管理目录
|
||||
/system,系统管理下的用户管理菜单 user。
|
||||
</template>
|
||||
<el-icon class="ml-1 cursor-pointer">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="formData.route_path" placeholder="请输入路由路径,如:/system" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="component">
|
||||
<template #label>
|
||||
<div class="flex-y-center">
|
||||
组件路径
|
||||
<el-tooltip placement="bottom" effect="light">
|
||||
<template #content>
|
||||
组件页面完整路径,相对于 src/views/,如 system/user/index,缺省后缀 .vue
|
||||
</template>
|
||||
<el-icon class="ml-1 cursor-pointer">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-input v-model="formData.component_path" placeholder="请输入组件路径,如system/user/index" style="width: 95%">
|
||||
<template v-if="formData.type == MenuTypeEnum.MENU" #prepend>src/views/</template>
|
||||
<template v-if="formData.type == MenuTypeEnum.MENU" #append>.vue</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.MENU">
|
||||
<template #label>
|
||||
<div class="flex-y-center">
|
||||
路由参数
|
||||
<el-tooltip placement="bottom" effect="light">
|
||||
<template #content>
|
||||
组件页面使用 `useRoute().query.参数名` 获取路由参数值。
|
||||
</template>
|
||||
<el-icon class="ml-1 cursor-pointer">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="!formData.params || (Array.isArray(formData.params) && formData.params.length === 0)">
|
||||
<el-button type="success" plain @click="formData.params = [{ key: '', value: '' }]">
|
||||
添加路由参数
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-for="(item, index) in formData.params" :key="index">
|
||||
<el-input v-model="item.key" placeholder="参数名" style="width: 100px" />
|
||||
|
||||
<span class="mx-1">=</span>
|
||||
|
||||
<el-input v-model="item.value" placeholder="参数值" style="width: 100px" />
|
||||
|
||||
<el-icon v-if="formData.params.indexOf(item) === formData.params.length - 1" class="ml-2 cursor-pointer color-[var(--el-color-success)]" style="vertical-align: -0.15em" @click="formData.params.push({ key: '', value: '' })">
|
||||
<CirclePlusFilled />
|
||||
</el-icon>
|
||||
<el-icon class="ml-2 cursor-pointer color-[var(--el-color-danger)]" style="vertical-align: -0.15em" @click="formData.params.splice(formData.params.indexOf(item), 1)">
|
||||
<DeleteFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type !== MenuTypeEnum.BUTTON">
|
||||
<template #label>
|
||||
<div class="flex-y-center">
|
||||
是否隐藏
|
||||
<el-tooltip placement="bottom" effect="light">
|
||||
<template #content>
|
||||
选择"是", 菜单中隐藏
|
||||
<br />
|
||||
选择"否",菜单中显示。
|
||||
<br />
|
||||
</template>
|
||||
<el-icon class="ml-1 cursor-pointer">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-radio-group v-model="formData.hidden">
|
||||
<el-radio :value="true">是</el-radio>
|
||||
<el-radio :value="false">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type === MenuTypeEnum.CATALOG || formData.type === MenuTypeEnum.MENU">
|
||||
<template #label>
|
||||
<div class="flex-y-center">
|
||||
始终显示
|
||||
<el-tooltip placement="bottom" effect="light">
|
||||
<template #content>
|
||||
选择"是",即使目录或菜单下只有一个子节点,也会显示父节点。
|
||||
<br />
|
||||
选择"否",如果目录或菜单下只有一个子节点,则只显示该子节点,隐藏父节点。
|
||||
<br />
|
||||
如果是叶子节点,请选择"否"。
|
||||
</template>
|
||||
<el-icon class="ml-1 cursor-pointer">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-radio-group v-model="formData.always_show">
|
||||
<el-radio :value="true">是</el-radio>
|
||||
<el-radio :value="false">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type === MenuTypeEnum.MENU" label="缓存页面">
|
||||
<el-radio-group v-model="formData.keep_alive">
|
||||
<el-radio :value="true">开启</el-radio>
|
||||
<el-radio :value="false">关闭</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="order">
|
||||
<el-input-number v-model="formData.order" controls-position="right" :min="1" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 权限标识 -->
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.BUTTON || formData.type === MenuTypeEnum.MENU" label="权限标识" prop="perm">
|
||||
<el-input v-model="formData.permission" placeholder="请输入权限标识,如sys:user:add" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type !== MenuTypeEnum.BUTTON" label="图标" prop="icon">
|
||||
<!-- 图标选择器 -->
|
||||
<icon-select v-model="formData.icon" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.CATALOG || formData.type === MenuTypeEnum.MENU" label="重定向" prop="redirect">
|
||||
<el-input v-model="formData.redirect" placeholder="请输入重定向路由" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type != MenuTypeEnum.BUTTON" label="常驻标签栏" prop="affix">
|
||||
<el-radio-group v-model="formData.affix">
|
||||
<el-radio :value="true">是</el-radio>
|
||||
<el-radio :value="false">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="true">启用</el-radio>
|
||||
<el-radio :value="false">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="formData.description" :rows="4" :maxlength="100" show-word-limit type="textarea" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" v-hasPerm="['system:menu:submit']" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else v-hasPerm="['system:menu:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "SysMenu",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { useAppStore } from "@/store/modules/app.store";
|
||||
import { useUserStore } from "@/store/modules/user.store";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
|
||||
import MenuAPI, { MenuPageQuery, MenuForm, MenuTable } from "@/api/module_system/menu";
|
||||
import { MenuTypeEnum } from "@/enums/system/menu.enum";
|
||||
import { formatTree } from "@/utils/common";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<MenuTable[]>([]);
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<MenuTable>({});
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<MenuPageQuery>({
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
});
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<MenuForm>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
type: MenuTypeEnum.CATALOG,
|
||||
icon: undefined,
|
||||
order: 999,
|
||||
permission: '',
|
||||
route_name: '',
|
||||
route_path: '',
|
||||
component_path: undefined,
|
||||
redirect: undefined,
|
||||
parent_id: undefined,
|
||||
keep_alive: false,
|
||||
hidden: false,
|
||||
always_show: false,
|
||||
title: '',
|
||||
params: undefined,
|
||||
affix: false,
|
||||
status: true,
|
||||
description: undefined,
|
||||
})
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'create' as 'create' | 'update' | 'detail',
|
||||
});
|
||||
|
||||
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
|
||||
|
||||
// 顶级菜单下拉选项
|
||||
const menuOptions = ref<OptionType[]>([]);
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
name: [
|
||||
{ required: true, message: "请输入菜单名称", trigger: "blur" },
|
||||
{ min: 2, max: 50, message: "长度 2 到 50 个字符", trigger: "blur" },
|
||||
],
|
||||
parent_id: [{ required: true, message: "请选择父级菜单", trigger: "blur" }],
|
||||
type: [{ required: true, message: "请选择菜单类型", trigger: "blur" }],
|
||||
order: [{ required: true, message: "请输入排序", trigger: "blur" }],
|
||||
permission: [{ required: true, message: "请输入权限标识", trigger: "blur" }],
|
||||
route_name: [{ required: true, message: "请输入路由名称", trigger: "blur" }],
|
||||
route_path: [
|
||||
{ required: true, message: "请输入路由路径", trigger: "blur" },
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (value && !value.startsWith('/')) {
|
||||
callback(new Error('目录和菜单路由必须以/开头'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
component_path: [{ required: true, message: "请输入组件路径", trigger: "blur" }],
|
||||
title: [
|
||||
{ required: true, message: "请输入菜单标题", trigger: "blur" },
|
||||
{ min: 2, max: 50, message: "长度 2 到 50 个字符", trigger: "blur" },
|
||||
],
|
||||
keep_alive: [{ required: true, message: "请选择是否缓存", trigger: "change" }],
|
||||
hidden: [{ required: true, message: "请选择是否隐藏", trigger: "change" }],
|
||||
always_show: [{ required: true, message: "请选择始终显示", trigger: "change" }],
|
||||
status: [{ required: true, message: "请选择状态", trigger: "change" }],
|
||||
});
|
||||
|
||||
// 选择表格的行菜单ID
|
||||
const selectedMenuId = ref<number | undefined>();
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh () {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await MenuAPI.getMenuList(queryFormData);
|
||||
pageTableData.value = response.data.data
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 修改菜单选项过滤逻辑,添加递归过滤函数
|
||||
const filterMenuTypes = (nodes: MenuTable[]) => {
|
||||
return nodes
|
||||
.filter(node => node.type === MenuTypeEnum.CATALOG || node.type === MenuTypeEnum.MENU)
|
||||
.map((node: any): any => ({
|
||||
...node,
|
||||
children: node.children ? filterMenuTypes(node.children) : []
|
||||
}));
|
||||
};
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await MenuAPI.getMenuList(queryFormData);
|
||||
pageTableData.value = response.data.data
|
||||
// 加载菜单选项,只显示目录、菜单
|
||||
menuOptions.value = formatTree(filterMenuTypes(response.data.data));
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
// 额外清空日期范围与时间查询参数
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: MenuForm = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
type: MenuTypeEnum.MENU,
|
||||
icon: undefined,
|
||||
order: 1,
|
||||
permission: '',
|
||||
route_name: '',
|
||||
route_path: '',
|
||||
component_path: '',
|
||||
redirect: '',
|
||||
parent_id: undefined,
|
||||
keep_alive: false,
|
||||
hidden: false,
|
||||
always_show: false,
|
||||
title: '',
|
||||
params: [] as { key: string; value: string }[],
|
||||
affix: false,
|
||||
status: true,
|
||||
description: undefined,
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
}
|
||||
|
||||
// 行点击事件
|
||||
async function handleRowClick(row: MenuTable) {
|
||||
selectedMenuId.value = row.id;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
//打开弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number, parentId?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await MenuAPI.getMenuDetail(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = "菜单详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === 'update') {
|
||||
dialogVisible.title = "修改菜单";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增菜单";
|
||||
formData.id = undefined;
|
||||
// 设置父级部门
|
||||
if (parentId) {
|
||||
formData.parent_id = parentId;
|
||||
}
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 菜单类型切换
|
||||
function handleMenuTypeChange() {
|
||||
// 如果菜单类型改变
|
||||
if (formData.type !== formData.type) {
|
||||
if (formData.type === MenuTypeEnum.MENU) {
|
||||
// 目录切换到菜单时,清空组件路径
|
||||
formData.component_path = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
try {
|
||||
if (id) {
|
||||
await MenuAPI.updateMenu(id, formData)
|
||||
} else {
|
||||
await MenuAPI.createMenu(formData)
|
||||
}
|
||||
await userStore.getUserInfo();
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await MenuAPI.deleteMenu(ids);
|
||||
await userStore.getUserInfo();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: boolean) {
|
||||
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await MenuAPI.batchAvailableMenu({ ids: selectIds.value, status });
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleQuery();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,592 @@
|
||||
<!-- 公告通知配置 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
|
||||
<el-form-item prop="notice_title" label="标题">
|
||||
<el-input v-model="queryFormData.notice_title" placeholder="请输入标题" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="notice_type" label="类型">
|
||||
<el-select v-model="queryFormData.notice_type" placeholder="请选择类型" style="width: 167.5px" clearable>
|
||||
<el-option
|
||||
v-for="item in dictStore.getDictArray('sys_notice_type')"
|
||||
:key="item.dict_value"
|
||||
:value="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
|
||||
<el-option value="true" label="启用" />
|
||||
<el-option value="false" label="停用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.creator"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button v-hasPerm="['system:notice:query']" type="primary" icon="search" native-type="submit">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button v-hasPerm="['system:notice:query']" icon="refresh" @click="handleResetQuery">
|
||||
重置
|
||||
</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="公告通知管理维护系统的公告和通知。">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
公告通知列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:notice:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:notice:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['system:notice:patch']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
|
||||
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button v-hasPerm="['system:notice:export']" type="warning" icon="download" circle @click="handleOpenExportsModal" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button v-hasPerm="['system:notice:refresh']" type="primary" icon="refresh" circle @click="handleRefresh" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-popover placement="bottom" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="danger" icon="operation" circle></el-button>
|
||||
</template>
|
||||
<el-scrollbar max-height="350px">
|
||||
<template v-for="column in tableColumns" :key="column.prop">
|
||||
<el-checkbox v-if="column.prop" v-model="column.show" :label="column.label" />
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-popover>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:系统配置列表 -->
|
||||
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'index')?.show" fixed label="序号" min-width="60" >
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'notice_title')?.show" label="通知标题" prop="notice_title" min-width="140" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'status')?.show" label="状态" prop="status" min-width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
|
||||
{{ scope.row.status === true ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'notice_type')?.show" label="类型" prop="notice_type" min-width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.notice_type === '1' ? 'primary' : 'warning'">
|
||||
{{ (scope.row.notice_type ? dictStore.getDictLabel('sys_notice_type', scope.row.notice_type) as any : undefined)?.dict_label || scope.row.notice_type }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'notice_content')?.show" label="内容" prop="notice_content" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'description')?.show" label="描述" prop="description" min-width="140" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'created_at')?.show" label="创建时间" prop="created_at" min-width="180" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'updated_at')?.show" label="更新时间" prop="updated_at" min-width="180" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'creator')?.show" key="creator" label="创建人" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.creator?.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-button v-hasPerm="['system:notice:detail']" type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button v-hasPerm="['system:notice:update']" type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
|
||||
<el-button v-hasPerm="['system:notice:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="标题" :span="2">
|
||||
{{ detailFormData.notice_title }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型" :span="2">
|
||||
<el-tag :type="detailFormData.notice_type === '1' ? 'primary' : 'warning'">
|
||||
{{ (detailFormData.notice_type ? dictStore.getDictLabel('sys_notice_type', detailFormData.notice_type) as any : undefined)?.dict_label || detailFormData.notice_type }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag :type="detailFormData.status ? 'success' : 'danger'">
|
||||
{{ detailFormData.status ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="内容" :span="2">
|
||||
{{ detailFormData.notice_content }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">
|
||||
{{ detailFormData.description }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人" :span="2">
|
||||
{{ detailFormData.creator?.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">
|
||||
{{ detailFormData.created_at }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">
|
||||
{{ detailFormData.updated_at }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right" :inline="true">
|
||||
<el-form-item label="标题" prop="notice_title">
|
||||
<el-input v-model="formData.notice_title" placeholder="请输入标题" :maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description" >
|
||||
<el-input v-model="formData.description" :rows="1" :maxlength="100" show-word-limit type="textarea" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="notice_type" class="w-50">
|
||||
<el-select v-model="formData.notice_type" placeholder="请选择类型" clearable>
|
||||
<el-option
|
||||
v-for="item in dictStore.getDictArray('sys_notice_type')"
|
||||
:key="item.dict_value"
|
||||
:value="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
/>
|
||||
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status" >
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="true">
|
||||
启用
|
||||
</el-radio>
|
||||
<el-radio :value="false">
|
||||
停用
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="内容" prop="notice_content">
|
||||
<WangEditor v-model="formData.notice_content" height="300px" />
|
||||
<!-- <el-input v-model="formData.notice_content" :rows="4" :maxlength="100" show-word-limit type="textarea" placeholder="请输入内容" /> -->
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" v-hasPerm="['system:notice:submit']" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else v-hasPerm="['system:notice:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<ExportModal v-model="exportsDialogVisible" :content-config="curdContentConfig" :selection-data="selectionRows" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDictStore } from "@/store/index";
|
||||
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const dictStore = useDictStore();
|
||||
defineOptions({
|
||||
name: "Notice",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import NoticeAPI, { NoticeTable, NoticeForm, NoticePageQuery } from "@/api/module_system/notice";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<NoticeTable[]>([]);
|
||||
|
||||
// 导出弹窗显示状态 & 选中行
|
||||
const exportsDialogVisible = ref(false);
|
||||
const selectionRows = ref<NoticeTable[]>([]);
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = ref([
|
||||
{ prop: 'selection', label: '选择框', show: true },
|
||||
{ prop: 'index', label: '序号', show: true },
|
||||
{ prop: 'notice_title', label: '标题', show: true },
|
||||
{ prop: 'notice_type', label: '类型', show: true },
|
||||
{ prop: 'notice_content', label: '内容', show: true },
|
||||
{ prop: 'status', label: '状态', show: true },
|
||||
{ prop: 'description', label: '描述', show: true },
|
||||
{ prop: 'created_at', label: '创建时间', show: true },
|
||||
{ prop: 'updated_at', label: '更新时间', show: true },
|
||||
{ prop: 'creator', label: '创建人', show: true },
|
||||
{ prop: 'operation', label: '操作', show: true }
|
||||
])
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<NoticeTable>({});
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<NoticePageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
notice_title: undefined,
|
||||
notice_type: undefined,
|
||||
status: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
// 创建人
|
||||
creator: undefined,
|
||||
});
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<NoticeForm>({
|
||||
id: undefined,
|
||||
notice_title: '',
|
||||
notice_type: '',
|
||||
notice_content: '',
|
||||
status: true,
|
||||
description: undefined,
|
||||
})
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'create' as 'create' | 'update' | 'detail',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
notice_title: [{ required: true, message: "请输入公告通知标题", trigger: "blur" }],
|
||||
notice_type: [{ required: true, message: "请选择公告通知类型", trigger: "blur" }],
|
||||
notice_content: [{ required: true, message: "请输入公告通知内容", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择公告通知状态", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh () {
|
||||
await loadingData();
|
||||
};
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await NoticeAPI.getNoticeList(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
// 额外清空日期范围与时间查询参数
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: NoticeForm = {
|
||||
id: undefined,
|
||||
notice_title: '',
|
||||
notice_type: '',
|
||||
notice_content: '',
|
||||
status: true,
|
||||
description: undefined,
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await NoticeAPI.getNoticeDetail(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = "公告通知详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === 'update') {
|
||||
dialogVisible.title = "修改公告通知";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增公告通知";
|
||||
formData.id = undefined;
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 提交表单(防抖)
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
if (id) {
|
||||
try {
|
||||
await NoticeAPI.updateNotice(id, { id, ...formData })
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await NoticeAPI.createNotice(formData)
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await NoticeAPI.deleteNotice(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 导出弹窗
|
||||
async function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: boolean) {
|
||||
if (selectIds.value.length) {
|
||||
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await NoticeAPI.batchAvailableNotice({ ids: selectIds.value, status });
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 导出字段
|
||||
const exportColumns = [
|
||||
{ prop: 'notice_title', label: '标题' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'notice_type', label: '类型' },
|
||||
{ prop: 'notice_content', label: '内容' },
|
||||
{ prop: 'description', label: '描述' },
|
||||
{ prop: 'created_at', label: '创建时间' },
|
||||
{ prop: 'updated_at', label: '更新时间' },
|
||||
];
|
||||
|
||||
// 导入/导出配置(用于导出弹窗)
|
||||
const curdContentConfig = {
|
||||
permPrefix: 'system:notice',
|
||||
cols: exportColumns as any,
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
if (typeof query.status === 'string') query.status = query.status === 'true';
|
||||
// notice_type 为字符串,无需转换;如果后端需要数字,可在此转换
|
||||
query.page_no = 1;
|
||||
query.page_size = 1000;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await NoticeAPI.getNoticeList(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
onMounted(async () => {
|
||||
// 加载字典数据
|
||||
await dictStore.getDict(['sys_notice_type']);
|
||||
// 加载表格数据
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,671 @@
|
||||
<template>
|
||||
<el-drawer v-model="drawerVisible" title="配置中心" :size="drawerSize" destroy-on-close @closed="onDrawerClosed">
|
||||
<el-tabs v-model="activeTabRef" type="border-card" >
|
||||
<!-- 网站配置 -->
|
||||
<el-tab-pane label="网站配置" name="website">
|
||||
<el-form :model="configState" label-suffix=":" label-width="auto" label-position="right">
|
||||
<!-- 系统配置 -->
|
||||
<el-divider>网站配置</el-divider>
|
||||
<div v-for="(item, key) in systemConfigs" :key="key">
|
||||
<el-form-item :label="item.config_name">
|
||||
<span class="flex items-center gap-2 w-full">
|
||||
<el-input
|
||||
v-model="item.config_value"
|
||||
:placeholder="t('common.inputText')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@input="markModified(key)"
|
||||
/>
|
||||
</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- logo配置 -->
|
||||
<el-divider>网站图标</el-divider>
|
||||
<div v-for="(item, key) in logoConfigs" :key="key">
|
||||
<el-form-item :label="item.config_name">
|
||||
<div class="flex items-center gap-2 w-full">
|
||||
<SingleImageUpload
|
||||
v-model="item.config_value"
|
||||
:data="{ type: key }"
|
||||
:name="'file'"
|
||||
:max-file-size="item.maxFileSize"
|
||||
:show-tip="true"
|
||||
:enable-preview="true"
|
||||
@success="(fileInfo: UploadFilePath) => handleUploadSuccess(fileInfo, key)"
|
||||
@error="handleUploadError"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="安全隐私" name="securityPrivacy">
|
||||
<el-form :model="configState" label-suffix=":" label-width="auto" label-position="right">
|
||||
<!-- 系统配置 -->
|
||||
<el-divider>安全隐私</el-divider>
|
||||
<div v-for="(item, key) in securityPrivacyConfigs" :key="key">
|
||||
<el-form-item :label="item.config_name">
|
||||
<span class="flex items-center gap-2 w-full">
|
||||
<el-input
|
||||
v-model="item.config_value"
|
||||
:placeholder="t('common.inputText')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@input="markModified(key)"
|
||||
/>
|
||||
</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="用户协议" name="userAgreement">
|
||||
<!-- 系统配置 -->
|
||||
<el-form :model="configState" label-suffix=":" label-width="auto" label-position="right">
|
||||
<el-divider>用户协议</el-divider>
|
||||
<div v-for="(item, key) in userAgreementConfigs" :key="key">
|
||||
<el-form-item :label="item.config_name">
|
||||
<span class="flex items-center gap-2 w-full">
|
||||
<el-input
|
||||
v-model="item.config_value"
|
||||
:placeholder="t('common.inputText')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@input="markModified(key)"
|
||||
/>
|
||||
</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="接口白名单" name="apiWhitelist">
|
||||
<el-form :model="configState" label-suffix=":" label-width="auto" label-position="right">
|
||||
<!-- 系统配置 -->
|
||||
<el-divider>接口白名单</el-divider>
|
||||
<div v-for="(item, key) in apiWhitelistConfigs" :key="key">
|
||||
<el-form-item :label="item.config_name">
|
||||
<div class="space-y-2">
|
||||
<div v-for="listItem in apiWhitelistItems" :key="listItem.id" class="flex items-center gap-2">
|
||||
<el-input
|
||||
v-model="listItem.value"
|
||||
:placeholder="'/api/v1/users/get'"
|
||||
clearable
|
||||
@input="markModified(key)"
|
||||
@blur="{
|
||||
if (!isValidApiPath(listItem.value) && listItem.value.trim()) {
|
||||
ElMessage.warning('请输入有效的接口路径格式(以/开头)');
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="minus"
|
||||
circle
|
||||
size="small"
|
||||
@click="removeApiWhitelistItem(listItem.id)"
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="plus"
|
||||
size="small"
|
||||
style="margin-top: 10px"
|
||||
@click="addApiWhitelistItem"
|
||||
>
|
||||
添加接口路径
|
||||
</el-button>
|
||||
<div class="text-xs text-gray-500 mt-2">
|
||||
配置说明:添加到白名单的接口路径无需登录即可访问,支持完整路径配置。
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="IP黑名单" name="ipBlacklist">
|
||||
<el-form :model="configState" label-suffix=":" label-width="auto" label-position="right">
|
||||
<!-- 系统配置 -->
|
||||
<el-divider>IP黑名单</el-divider>
|
||||
<div v-for="(item, key) in ipBlacklistConfigs" :key="key">
|
||||
<el-form-item :label="item.config_name">
|
||||
<div class="space-y-2">
|
||||
<div v-for="listItem in ipBlacklistItems" :key="listItem.id" class="flex items-center gap-2">
|
||||
<el-input
|
||||
v-model="listItem.value"
|
||||
:placeholder="'192.168.1.1'"
|
||||
clearable
|
||||
style="flex: 1"
|
||||
@input="markModified(key)"
|
||||
@blur="{
|
||||
if (!isValidIp(listItem.value) && listItem.value.trim()) {
|
||||
ElMessage.warning('请输入有效的IP地址格式');
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="minus"
|
||||
circle
|
||||
size="small"
|
||||
@click="removeIpBlacklistItem(listItem.id)"
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="plus"
|
||||
size="small"
|
||||
style="margin-top: 10px"
|
||||
@click="addIpBlacklistItem"
|
||||
>
|
||||
添加IP地址
|
||||
</el-button>
|
||||
<div class="text-xs text-gray-500 mt-2">
|
||||
配置说明:添加到黑名单的IP地址将无法访问系统,支持单个IP配置。
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="演示环境配置" name="demo">
|
||||
<el-form :model="configState" label-suffix=":" label-width="auto" label-position="right">
|
||||
<!-- 系统配置 -->
|
||||
<el-divider>演示环境配置</el-divider>
|
||||
<div v-for="(item, key) in demoConfigs" :key="key">
|
||||
<el-form-item :label="item.config_name">
|
||||
<!-- 演示模式开关 -->
|
||||
<template v-if="key === 'demo_enable'">
|
||||
<el-switch
|
||||
inline-prompt
|
||||
active-text="启用"
|
||||
inactive-text="禁用"
|
||||
:model-value="item.config_value === 'true'"
|
||||
@update:model-value="(value) => {
|
||||
item.config_value = value ? 'true' : 'false';
|
||||
markModified(key);
|
||||
}"
|
||||
|
||||
/>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
配置说明:启用后系统将进入演示模式,部分功能可能受限。
|
||||
</div>
|
||||
</template>
|
||||
<!-- IP白名单 -->
|
||||
<template v-else-if="key === 'ip_white_list'">
|
||||
<div class="space-y-2">
|
||||
<div v-for="listItem in demoIpWhitelistItems" :key="listItem.id" class="flex items-center gap-2">
|
||||
<el-input
|
||||
v-model="listItem.value"
|
||||
:placeholder="'192.168.1.1'"
|
||||
clearable
|
||||
style="flex: 1"
|
||||
@input="markModified(key)"
|
||||
@blur="{
|
||||
if (!isValidIp(listItem.value) && listItem.value.trim()) {
|
||||
ElMessage.warning('请输入有效的IP地址格式');
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="minus"
|
||||
circle
|
||||
size="small"
|
||||
@click="removeDemoIpWhitelistItem(listItem.id)"
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="plus"
|
||||
size="small"
|
||||
style="margin-top: 10px"
|
||||
@click="addDemoIpWhitelistItem"
|
||||
>
|
||||
添加IP地址
|
||||
</el-button>
|
||||
<div class="text-xs text-gray-500 mt-2">
|
||||
配置说明:演示模式下,只有白名单中的IP地址可以访问系统,支持单个IP配置。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<!-- 其他配置项 -->
|
||||
<template v-else>
|
||||
<el-input
|
||||
v-model="item.config_value"
|
||||
:placeholder="t('common.inputText')"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@input="markModified(key)"
|
||||
/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template #footer>
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-hasPerm="['system:config:update']" type="primary" :disabled="!hasChanges" @click="submitChanges">保存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import ParamsAPI, { type ConfigTable } from '@/api/module_system/params';
|
||||
import { useConfigStore } from "@/store";
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import SingleImageUpload from '@/components/Upload/SingleImageUpload.vue';
|
||||
import { useAppStore } from "@/store/modules/app.store";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
|
||||
// 定义列表项类型
|
||||
interface ListItem {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
// 生成唯一ID
|
||||
const generateId = () => {
|
||||
return Math.random().toString(36).substr(2, 9);
|
||||
};
|
||||
|
||||
// IP地址验证函数
|
||||
const isValidIp = (ip: string): boolean => {
|
||||
const ipRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
return ipRegex.test(ip);
|
||||
};
|
||||
|
||||
// 接口路径验证函数
|
||||
const isValidApiPath = (path: string): boolean => {
|
||||
const pathRegex = /^\/[\w\-/]+$/;
|
||||
return pathRegex.test(path);
|
||||
}
|
||||
|
||||
const appStore = useAppStore();
|
||||
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "60%" : "60%"));
|
||||
|
||||
const t = useI18n().t;
|
||||
const configStore = useConfigStore();
|
||||
const activeTabRef = ref('website')
|
||||
|
||||
// 与父组件的 v-model 同步
|
||||
const props = defineProps<{ modelValue: boolean }>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const drawerVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val: boolean) => emit('update:modelValue', val)
|
||||
});
|
||||
|
||||
// 配置状态管理
|
||||
const configState = reactive<ConfigTable>({
|
||||
id: undefined,
|
||||
config_name: '',
|
||||
config_key: '',
|
||||
config_value: '',
|
||||
config_type: undefined,
|
||||
description: ''
|
||||
});
|
||||
|
||||
// 记录修改过的字段
|
||||
const modifiedFields = reactive<Record<string, boolean>>({});
|
||||
|
||||
// 标记字段为已修改
|
||||
const markModified = (key: string) => {
|
||||
modifiedFields[key] = true;
|
||||
};
|
||||
|
||||
// 判断是否有修改
|
||||
const hasChanges = computed(() => Object.keys(modifiedFields).length > 0);
|
||||
|
||||
// 提交修改
|
||||
const submitChanges = async () => {
|
||||
const keysToSubmit = Object.keys(modifiedFields);
|
||||
if (keysToSubmit.length === 0) return;
|
||||
|
||||
try {
|
||||
// 准备提交数据
|
||||
// 1. 处理接口白名单
|
||||
if ('white_api_list_path' in modifiedFields && apiWhitelistConfigs.value.white_api_list_path?.id) {
|
||||
const apiWhitelistArray = apiWhitelistItems.value
|
||||
.map(item => item.value.trim())
|
||||
.filter(Boolean);
|
||||
// 转换为JSON字符串格式保存
|
||||
const apiWhitelistJson = JSON.stringify(apiWhitelistArray);
|
||||
await ParamsAPI.updateConfig(apiWhitelistConfigs.value.white_api_list_path.id, {
|
||||
...apiWhitelistConfigs.value.white_api_list_path,
|
||||
config_value: apiWhitelistJson
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 处理IP黑名单
|
||||
if ('ip_black_list' in modifiedFields && ipBlacklistConfigs.value.ip_black_list?.id) {
|
||||
const ipBlacklistArray = ipBlacklistItems.value
|
||||
.map(item => item.value.trim())
|
||||
.filter(Boolean);
|
||||
// 转换为JSON字符串格式保存
|
||||
const ipBlacklistJson = JSON.stringify(ipBlacklistArray);
|
||||
await ParamsAPI.updateConfig(ipBlacklistConfigs.value.ip_black_list.id, {
|
||||
...ipBlacklistConfigs.value.ip_black_list,
|
||||
config_value: ipBlacklistJson
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 处理演示环境IP白名单
|
||||
if ('ip_white_list' in modifiedFields && demoConfigs.value.ip_white_list?.id) {
|
||||
const demoIpWhitelistArray = demoIpWhitelistItems.value
|
||||
.map(item => item.value.trim())
|
||||
.filter(Boolean);
|
||||
// 转换为JSON字符串格式保存
|
||||
const demoIpWhitelistJson = JSON.stringify(demoIpWhitelistArray);
|
||||
await ParamsAPI.updateConfig(demoConfigs.value.ip_white_list.id, {
|
||||
...demoConfigs.value.ip_white_list,
|
||||
config_value: demoIpWhitelistJson
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 处理其他配置项
|
||||
const otherKeys = keysToSubmit.filter(key =>
|
||||
!['white_api_list_path', 'ip_black_list', 'ip_white_list'].includes(key)
|
||||
);
|
||||
const otherUpdatePromises = otherKeys.map(key => {
|
||||
const item = systemConfigs.value[key as keyof typeof systemConfigs.value] ||
|
||||
logoConfigs.value[key as keyof typeof logoConfigs.value] ||
|
||||
securityPrivacyConfigs.value[key as keyof typeof securityPrivacyConfigs.value] ||
|
||||
userAgreementConfigs.value[key as keyof typeof userAgreementConfigs.value] ||
|
||||
demoConfigs.value[key as keyof typeof demoConfigs.value];
|
||||
return item && item.id ? ParamsAPI.updateConfig(item.id, { ...item }) : Promise.resolve();
|
||||
});
|
||||
await Promise.all(otherUpdatePromises);
|
||||
|
||||
// 清除已提交的修改标记
|
||||
keysToSubmit.forEach(key => {
|
||||
delete modifiedFields[key];
|
||||
});
|
||||
|
||||
// 重新加载配置数据
|
||||
await configStore.getConfig();
|
||||
initializeLists();
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消修改:重置所有修改字段的状态并恢复初始值
|
||||
const resetForm = () => {
|
||||
// 重置动态列表
|
||||
initializeLists();
|
||||
|
||||
// 重置其他配置项
|
||||
const keysToReset = Object.keys(modifiedFields);
|
||||
for (const key of keysToReset) {
|
||||
if (systemConfigs.value[key as keyof typeof systemConfigs.value]) {
|
||||
systemConfigs.value[key as keyof typeof systemConfigs.value].config_value = configStore.configData[key as keyof typeof configStore.configData]?.config_value || '';
|
||||
} else if (logoConfigs.value[key as keyof typeof logoConfigs.value]) {
|
||||
logoConfigs.value[key as keyof typeof logoConfigs.value].config_value = configStore.configData[key as keyof typeof configStore.configData]?.config_value || '';
|
||||
} else if (securityPrivacyConfigs.value[key as keyof typeof securityPrivacyConfigs.value]) {
|
||||
securityPrivacyConfigs.value[key as keyof typeof securityPrivacyConfigs.value].config_value = configStore.configData[key as keyof typeof configStore.configData]?.config_value || '';
|
||||
} else if (userAgreementConfigs.value[key as keyof typeof userAgreementConfigs.value]) {
|
||||
userAgreementConfigs.value[key as keyof typeof userAgreementConfigs.value].config_value = configStore.configData[key as keyof typeof configStore.configData]?.config_value || '';
|
||||
} else if (demoConfigs.value[key as keyof typeof demoConfigs.value]) {
|
||||
// 除了IP白名单外的演示配置项
|
||||
if (key !== 'ip_white_list') {
|
||||
demoConfigs.value[key as keyof typeof demoConfigs.value].config_value = configStore.configData[key as keyof typeof configStore.configData]?.config_value || '';
|
||||
}
|
||||
}
|
||||
delete modifiedFields[key];
|
||||
}
|
||||
ElMessageBox.close();
|
||||
};
|
||||
|
||||
async function handleCloseDialog() {
|
||||
// 仅关闭抽屉,等待关闭动画结束后再重置
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
|
||||
function onDrawerClosed() {
|
||||
// 抽屉关闭动画结束后再执行重置,避免打断动画
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 系统配置项
|
||||
const systemConfigs = computed(() => ({
|
||||
sys_web_title: configStore.configData.sys_web_title,
|
||||
sys_web_version: configStore.configData.sys_web_version,
|
||||
sys_web_description: configStore.configData.sys_web_description,
|
||||
}));
|
||||
|
||||
// 安全隐私配置项
|
||||
const securityPrivacyConfigs = computed(() => ({
|
||||
sys_help_doc: configStore.configData.sys_help_doc,
|
||||
sys_git_code: configStore.configData.sys_git_code,
|
||||
sys_keep_record: configStore.configData.sys_keep_record,
|
||||
sys_web_copyright: configStore.configData.sys_web_copyright,
|
||||
sys_web_privacy: configStore.configData.sys_web_privacy,
|
||||
}));
|
||||
|
||||
// 用户协议配置项
|
||||
const userAgreementConfigs = computed(() => ({
|
||||
sys_web_clause: configStore.configData.sys_web_clause,
|
||||
}));
|
||||
|
||||
// 接口白名单配置 - 动态管理
|
||||
const apiWhitelistItems = ref<ListItem[]>([]);
|
||||
// IP黑名单配置 - 动态管理
|
||||
const ipBlacklistItems = ref<ListItem[]>([]);
|
||||
// IP白名单配置 - 动态管理
|
||||
const demoIpWhitelistItems = ref<ListItem[]>([]);
|
||||
|
||||
// 从配置数据初始化列表
|
||||
const initializeLists = () => {
|
||||
// 初始化接口白名单
|
||||
const apiWhitelistStr = configStore.configData.white_api_list_path?.config_value || '';
|
||||
try {
|
||||
// 尝试解析为JSON数组
|
||||
const apiWhitelistArray = JSON.parse(apiWhitelistStr);
|
||||
if (Array.isArray(apiWhitelistArray)) {
|
||||
apiWhitelistItems.value = apiWhitelistArray
|
||||
.filter(item => typeof item === 'string' && item.trim())
|
||||
.map(item => ({ id: generateId(), value: item.trim() }));
|
||||
} else {
|
||||
// 如果不是数组,回退到按换行符分割
|
||||
apiWhitelistItems.value = apiWhitelistStr
|
||||
? apiWhitelistStr.split('\n')
|
||||
.filter(item => item.trim())
|
||||
.map(item => ({ id: generateId(), value: item.trim() }))
|
||||
: [{ id: generateId(), value: '' }];
|
||||
}
|
||||
} catch {
|
||||
// 解析失败,回退到按换行符分割
|
||||
apiWhitelistItems.value = apiWhitelistStr
|
||||
? apiWhitelistStr.split('\n')
|
||||
.filter(item => item.trim())
|
||||
.map(item => ({ id: generateId(), value: item.trim() }))
|
||||
: [{ id: generateId(), value: '' }];
|
||||
}
|
||||
|
||||
// 初始化IP黑名单
|
||||
const ipBlacklistStr = configStore.configData.ip_black_list?.config_value || '';
|
||||
try {
|
||||
// 尝试解析为JSON数组
|
||||
const ipBlacklistArray = JSON.parse(ipBlacklistStr);
|
||||
if (Array.isArray(ipBlacklistArray)) {
|
||||
ipBlacklistItems.value = ipBlacklistArray
|
||||
.filter(item => typeof item === 'string' && item.trim())
|
||||
.map(item => ({ id: generateId(), value: item.trim() }));
|
||||
} else {
|
||||
// 如果不是数组,回退到按换行符分割
|
||||
ipBlacklistItems.value = ipBlacklistStr
|
||||
? ipBlacklistStr.split('\n')
|
||||
.filter(item => item.trim())
|
||||
.map(item => ({ id: generateId(), value: item.trim() }))
|
||||
: [{ id: generateId(), value: '' }];
|
||||
}
|
||||
} catch {
|
||||
// 解析失败,回退到按换行符分割
|
||||
ipBlacklistItems.value = ipBlacklistStr
|
||||
? ipBlacklistStr.split('\n')
|
||||
.filter(item => item.trim())
|
||||
.map(item => ({ id: generateId(), value: item.trim() }))
|
||||
: [{ id: generateId(), value: '' }];
|
||||
}
|
||||
|
||||
// 初始化演示环境IP白名单
|
||||
const demoIpWhitelistStr = configStore.configData.ip_white_list?.config_value || '';
|
||||
try {
|
||||
// 尝试解析为JSON数组
|
||||
const demoIpWhitelistArray = JSON.parse(demoIpWhitelistStr);
|
||||
if (Array.isArray(demoIpWhitelistArray)) {
|
||||
demoIpWhitelistItems.value = demoIpWhitelistArray
|
||||
.filter(item => typeof item === 'string' && item.trim())
|
||||
.map(item => ({ id: generateId(), value: item.trim() }));
|
||||
} else {
|
||||
// 如果不是数组,回退到按换行符分割
|
||||
demoIpWhitelistItems.value = demoIpWhitelistStr
|
||||
? demoIpWhitelistStr.split('\n')
|
||||
.filter(item => item.trim())
|
||||
.map(item => ({ id: generateId(), value: item.trim() }))
|
||||
: [{ id: generateId(), value: '' }];
|
||||
}
|
||||
} catch {
|
||||
// 解析失败,回退到按换行符分割
|
||||
demoIpWhitelistItems.value = demoIpWhitelistStr
|
||||
? demoIpWhitelistStr.split('\n')
|
||||
.filter(item => item.trim())
|
||||
.map(item => ({ id: generateId(), value: item.trim() }))
|
||||
: [{ id: generateId(), value: '' }];
|
||||
}
|
||||
};
|
||||
|
||||
// 添加接口白名单项
|
||||
const addApiWhitelistItem = () => {
|
||||
apiWhitelistItems.value.push({ id: generateId(), value: '' });
|
||||
markModified('white_api_list_path');
|
||||
};
|
||||
|
||||
// 移除接口白名单项
|
||||
const removeApiWhitelistItem = (id: string) => {
|
||||
if (apiWhitelistItems.value.length <= 1) {
|
||||
ElMessage.warning('至少需要保留一个接口白名单配置');
|
||||
return;
|
||||
}
|
||||
apiWhitelistItems.value = apiWhitelistItems.value.filter(item => item.id !== id);
|
||||
markModified('white_api_list_path');
|
||||
};
|
||||
|
||||
// 添加IP黑名单项
|
||||
const addIpBlacklistItem = () => {
|
||||
ipBlacklistItems.value.push({ id: generateId(), value: '' });
|
||||
markModified('ip_black_list');
|
||||
};
|
||||
|
||||
// 移除IP黑名单项
|
||||
const removeIpBlacklistItem = (id: string) => {
|
||||
if (ipBlacklistItems.value.length <= 1) {
|
||||
ElMessage.warning('至少需要保留一个IP黑名单配置');
|
||||
return;
|
||||
}
|
||||
ipBlacklistItems.value = ipBlacklistItems.value.filter(item => item.id !== id);
|
||||
markModified('ip_black_list');
|
||||
};
|
||||
|
||||
// 添加演示环境IP白名单项
|
||||
const addDemoIpWhitelistItem = () => {
|
||||
demoIpWhitelistItems.value.push({ id: generateId(), value: '' });
|
||||
markModified('ip_white_list');
|
||||
};
|
||||
|
||||
// 移除演示环境IP白名单项
|
||||
const removeDemoIpWhitelistItem = (id: string) => {
|
||||
if (demoIpWhitelistItems.value.length <= 1) {
|
||||
ElMessage.warning('至少需要保留一个IP白名单配置');
|
||||
return;
|
||||
}
|
||||
demoIpWhitelistItems.value = demoIpWhitelistItems.value.filter(item => item.id !== id);
|
||||
markModified('ip_white_list');
|
||||
};
|
||||
|
||||
// 接口白名单配置项
|
||||
const apiWhitelistConfigs = computed(() => ({
|
||||
white_api_list_path: configStore.configData.white_api_list_path,
|
||||
}));
|
||||
|
||||
// IP黑名单配置项
|
||||
const ipBlacklistConfigs = computed(() => ({
|
||||
ip_black_list: configStore.configData.ip_black_list,
|
||||
}));
|
||||
|
||||
// 演示环境配置项
|
||||
const demoConfigs = computed(() => ({
|
||||
demo_enable: configStore.configData.demo_enable,
|
||||
ip_white_list: configStore.configData.ip_white_list,
|
||||
}));
|
||||
|
||||
// 在组件挂载时初始化列表
|
||||
onMounted(() => {
|
||||
initializeLists();
|
||||
});
|
||||
|
||||
// logo配置项
|
||||
const logoConfigs = computed(() => ({
|
||||
sys_web_logo: {
|
||||
...configStore.configData.sys_web_logo,
|
||||
maxFileSize: 5,
|
||||
},
|
||||
sys_web_favicon: {
|
||||
...configStore.configData.sys_web_favicon,
|
||||
maxFileSize: 5,
|
||||
},
|
||||
sys_login_background: {
|
||||
...configStore.configData.sys_login_background,
|
||||
maxFileSize: 10,
|
||||
},
|
||||
}));
|
||||
|
||||
// 图片上传成功的回调处理
|
||||
const handleUploadSuccess = (fileInfo: UploadFilePath, type: string) => {
|
||||
const fileUrl = fileInfo.file_url;
|
||||
if (type in configStore.configData) {
|
||||
(configStore.configData as any)[type].config_value = fileUrl;
|
||||
}
|
||||
if (type in systemConfigs.value) {
|
||||
(systemConfigs.value as any)[type].config_value = fileUrl;
|
||||
} else if (type in logoConfigs.value) {
|
||||
(logoConfigs.value as any)[type].config_value = fileUrl;
|
||||
}
|
||||
markModified(type);
|
||||
};
|
||||
|
||||
// 图片上传失败的回调处理
|
||||
const handleUploadError = (error: any) => {
|
||||
console.error('上传失败:', error.message || '未知错误');
|
||||
ElMessage.error(`上传失败:${error.message || '请稍后重试'}`);
|
||||
};
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
configStore.getConfig();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.gap-4 {
|
||||
gap: 1rem;
|
||||
}
|
||||
.mt-6 {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,502 @@
|
||||
<!-- 系统配置 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
|
||||
<el-form-item prop="config_name" label="配置名称">
|
||||
<el-input v-model="queryFormData.config_name" placeholder="请输入配置名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="config_key" label="配置键名">
|
||||
<el-input v-model="queryFormData.config_key" placeholder="请输入配置键名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="config_type" label="系统内置">
|
||||
<el-select v-model="queryFormData.config_type" placeholder="请选择系统内置" style="width: 167.5px" clearable>
|
||||
<el-option value="true" label="是" />
|
||||
<el-option value="false" label="否" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.creator"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button v-hasPerm="['system:config:query']" type="primary" icon="search" native-type="submit">查询</el-button>
|
||||
<el-button v-hasPerm="['system:config:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="配置管理,包括系统名称、系统描述、系统版本、系统logo等。">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
系统配置列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:config:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:config:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button v-hasPerm="['system:config:export']" type="warning" icon="download" circle @click="handleOpenExportsModal"/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button v-hasPerm="['system:config:refresh']" type="primary" icon="refresh" circle @click="handleRefresh"/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['system:config:filter']" trigger="click">
|
||||
<el-button type="default" icon="operation" circle />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="column in tableColumns" :key="column.prop" :command="column">
|
||||
<el-checkbox v-model="column.show">{{ column.label }}</el-checkbox>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:系统配置列表 -->
|
||||
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'index')?.show" type="index" fixed label="序号" min-width="60">
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'config_name')?.show" key="config_name" label="配置名称" prop="config_name" min-width="100" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'config_key')?.show" key="config_key" label="配置键" prop="config_key" min-width="200" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'config_value')?.show" key="config_value" label="配置值" prop="config_value" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'config_type')?.show" key="config_type" label="系统内置" prop="config_type" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.config_type" type="success">是</el-tag>
|
||||
<el-tag v-else type="danger">否</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'description')?.show" key="description" label="描述" prop="description" min-width="120" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'created_at')?.show" key="created_at" label="创建时间" prop="created_at" min-width="200" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'updated_at')?.show" key="updated_at" label="更新时间" prop="updated_at" min-width="200" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'creator')?.show" key="creator" label="创建人" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.creator?.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-button v-hasPerm="['system:config:detail']" type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button v-hasPerm="['system:config:update']" type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
|
||||
<el-button v-hasPerm="['system:config:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="配置名称" :span="2">{{ detailFormData.config_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="系统内置" :span="2">
|
||||
<el-tag v-if="detailFormData.config_type" type="success">是</el-tag>
|
||||
<el-tag v-else type="danger">否</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="配置键" :span="2">{{ detailFormData.config_key }}</el-descriptions-item>
|
||||
<el-descriptions-item label="配置值" :span="2">{{ detailFormData.config_value }}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">{{ detailFormData.description }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人" :span="2">{{ detailFormData.creator?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ detailFormData.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ detailFormData.updated_at }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
|
||||
<el-form-item label="配置名称" prop="config_name">
|
||||
<el-input v-model="formData.config_name" placeholder="请输入配置名称" :maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置键" prop="config_key">
|
||||
<el-input v-model="formData.config_key" placeholder="请输入配置键" :maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置值" prop="config_value">
|
||||
<el-input v-model="formData.config_value" placeholder="请输入配置值" :maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="系统内置" prop="config_type">
|
||||
<el-radio-group v-model="formData.config_type">
|
||||
<el-radio :value="true">是</el-radio>
|
||||
<el-radio :value="false">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="formData.description" :rows="4" :maxlength="100" show-word-limit type="textarea" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" v-hasPerm="['system:config:create']" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else v-hasPerm="['system:config:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<ExportModal v-model="exportsDialogVisible" :content-config="curdContentConfig" :selection-data="selectionRows" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "Params",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import ParamsAPI, { ConfigTable, ConfigForm, ConfigPageQuery } from "@/api/module_system/params";
|
||||
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<ConfigTable[]>([]);
|
||||
|
||||
// 导出弹窗显示状态 & 选中行
|
||||
const exportsDialogVisible = ref(false);
|
||||
const selectionRows = ref<ConfigTable[]>([]);
|
||||
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = ref([
|
||||
{ prop: 'selection', label: '选择框', show: true },
|
||||
{ prop: 'index', label: '序号', show: true },
|
||||
{ prop: 'config_name', label: '配置名称', show: true },
|
||||
{ prop: 'config_key', label: '配置键', show: true },
|
||||
{ prop: 'config_value', label: '配置值', show: true },
|
||||
{ prop: 'config_type', label: '系统内置', show: true },
|
||||
{ prop: 'description', label: '描述', show: true },
|
||||
{ prop: 'created_at', label: '创建时间', show: true },
|
||||
{ prop: 'updated_at', label: '更新时间', show: true },
|
||||
{ prop: 'creator', label: '创建人', show: true },
|
||||
{ prop: 'operation', label: '操作', show: true }
|
||||
])
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<ConfigTable>({});
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<ConfigPageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
config_name: undefined,
|
||||
config_key: undefined,
|
||||
config_type: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
// 创建人
|
||||
creator: undefined,
|
||||
});
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<ConfigForm>({
|
||||
id: undefined,
|
||||
config_name: '',
|
||||
config_key: '',
|
||||
config_value: '',
|
||||
config_type: false,
|
||||
description: ''
|
||||
});
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'create' as 'create' | 'update' | 'detail',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
config_name: [{ required: true, message: "请输入系统配置名称", trigger: "blur" }],
|
||||
config_key: [{ required: true, message: "请输入系统配置键", trigger: "blur" }],
|
||||
config_value: [{ required: true, message: "请输入系统配置值", trigger: "blur" }],
|
||||
config_type: [{ required: true, message: "请选择系统配置类型", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh () {
|
||||
await loadingData();
|
||||
};
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await ParamsAPI.getConfigList(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
// 额外清空日期范围与时间查询参数
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: ConfigForm = {
|
||||
id: undefined,
|
||||
config_name: '',
|
||||
config_key: '',
|
||||
config_value: '',
|
||||
config_type: false,
|
||||
description: ''
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开系统配置弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await ParamsAPI.getConfigDetail(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = "系统配置详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === 'update') {
|
||||
dialogVisible.title = "修改系统配置";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增系统配置";
|
||||
formData.id = undefined;
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 新增、编辑弹窗处理
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
if (id) {
|
||||
try {
|
||||
await ParamsAPI.updateConfig(id, formData)
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await ParamsAPI.createConfig(formData)
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await ParamsAPI.deleteConfig(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 导出弹窗
|
||||
async function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 导出字段
|
||||
const exportColumns = [
|
||||
{ prop: 'config_name', label: '配置名称' },
|
||||
{ prop: 'config_key', label: '配置键' },
|
||||
{ prop: 'config_value', label: '配置值' },
|
||||
{ prop: 'config_type', label: '系统内置' },
|
||||
{ prop: 'description', label: '描述' },
|
||||
{ prop: 'created_at', label: '创建时间' },
|
||||
{ prop: 'updated_at', label: '更新时间' },
|
||||
];
|
||||
|
||||
// 导入/导出配置(用于导出弹窗)
|
||||
const curdContentConfig = {
|
||||
permPrefix: 'system:config',
|
||||
cols: exportColumns as any,
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
if (typeof query.config_type === 'string') {
|
||||
query.config_type = query.config_type === 'true';
|
||||
}
|
||||
query.page_no = 1;
|
||||
query.page_size = 1000;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await ParamsAPI.getConfigList(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
onMounted(() => {
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,556 @@
|
||||
<!-- 岗位管理 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
|
||||
<el-form-item prop="name" label="岗位名称">
|
||||
<el-input v-model="queryFormData.name" placeholder="请输入岗位名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
|
||||
<el-option value="true" label="启用" />
|
||||
<el-option value="false" label="停用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.creator"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button v-hasPerm="['system:position:query']" type="primary" icon="search" native-type="submit">查询</el-button>
|
||||
<el-button v-hasPerm="['system:position:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="岗位管理维护系统的岗位信息。">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
岗位列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:position:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:position:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['system:position:patch']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu v-hasPerm="['system:position:filter']">
|
||||
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
|
||||
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button v-hasPerm="['system:position:export']" type="warning" icon="download" circle @click="handleOpenExportsModal" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button v-hasPerm="['system:position:refresh']" type="primary" icon="refresh" circle @click="handleRefresh" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-popover placement="bottom" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="danger" icon="operation" circle></el-button>
|
||||
</template>
|
||||
<el-scrollbar max-height="350px">
|
||||
<template v-for="column in tableColumns" :key="column.prop">
|
||||
<el-checkbox v-if="column.prop" v-model="column.show" :label="column.label" />
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-popover>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:系统配置列表 -->
|
||||
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'index')?.show" type="index" fixed label="序号" min-width="60">
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'name')?.show" key="name" label="岗位名称" prop="name" min-width="100" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'status')?.show" key="status" label="状态" prop="status" min-width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
|
||||
{{ scope.row.status === true ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'order')?.show" key="order" label="岗位排序" prop="order" min-width="80" show-overflow-tooltip />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'description')?.show" key="description" label="描述" prop="description" min-width="120" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'created_at')?.show" key="created_at" label="创建时间" prop="created_at" min-width="200" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'updated_at')?.show" key="updated_at" label="更新时间" prop="updated_at" min-width="200" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'creator')?.show" key="creator" label="创建人" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.creator?.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-button v-hasPerm="['system:position:detail']" type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button v-hasPerm="['system:position:update']" type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
|
||||
<el-button v-hasPerm="['system:position:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="岗位名称" :span="2">{{ detailFormData.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="排序" :span="2">
|
||||
{{ detailFormData.order }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag v-if="detailFormData.status" type="success">启用</el-tag>
|
||||
<el-tag v-else type="danger">停用</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人" :span="2">{{ detailFormData.creator?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ detailFormData.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ detailFormData.updated_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="4">{{ detailFormData.description }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
|
||||
<el-form-item label="岗位名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入岗位名称" :maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="order">
|
||||
<el-input-number v-model="formData.order" controls-position="right" :min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="true">启用</el-radio>
|
||||
<el-radio :value="false">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="formData.description" :rows="4" :maxlength="100" show-word-limit type="textarea" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" v-hasPerm="['system:position:submit']" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else v-hasPerm="['system:position:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<ExportModal v-model="exportsDialogVisible" :content-config="curdContentConfig" :selection-data="selectionRows" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "Position",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import PositionAPI, { PositionTable, PositionForm, PositionPageQuery } from "@/api/module_system/position";
|
||||
import { useUserStore } from "@/store";
|
||||
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<PositionTable[]>([]);
|
||||
|
||||
// 导出弹窗显示状态 & 选中行
|
||||
const exportsDialogVisible = ref(false);
|
||||
const selectionRows = ref<PositionTable[]>([]);
|
||||
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = ref([
|
||||
{ prop: 'selection', label: '选择框', show: true },
|
||||
{ prop: 'index', label: '序号', show: true },
|
||||
{ prop: 'name', label: '岗位名称', show: true },
|
||||
{ prop: 'order', label: '岗位排序', show: true },
|
||||
{ prop: 'status', label: '状态', show: true },
|
||||
{ prop: 'description', label: '描述', show: true },
|
||||
{ prop: 'created_at', label: '创建时间', show: true },
|
||||
{ prop: 'updated_at', label: '更新时间', show: true },
|
||||
{ prop: 'creator', label: '创建人', show: true },
|
||||
{ prop: 'operation', label: '操作', show: true }
|
||||
])
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<PositionTable>({});
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<PositionPageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
name: '',
|
||||
status: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
// 创建人
|
||||
creator: undefined,
|
||||
});
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<PositionForm>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
order: 1,
|
||||
status: true,
|
||||
description: undefined,
|
||||
})
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'create' as 'create' | 'update' | 'detail',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: "请输入岗位名称", trigger: "blur" }],
|
||||
order: [{ required: true, message: "请输入岗位排序", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择岗位状态", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh () {
|
||||
await loadingData();
|
||||
};
|
||||
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await PositionAPI.getPositionList(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
// 额外清空日期范围与时间查询参数
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: PositionForm = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
order: 1,
|
||||
status: true,
|
||||
description: undefined,
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开系统配置弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await PositionAPI.getPositionDetail(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = "岗位详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === 'update') {
|
||||
dialogVisible.title = "修改岗位";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增岗位";
|
||||
formData.id = undefined;
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 新增、编辑弹窗处理
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
try {
|
||||
if (id) {
|
||||
await PositionAPI.updatePosition(id, { id, ...formData })
|
||||
} else {
|
||||
await PositionAPI.createPosition(formData)
|
||||
}
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleResetQuery();
|
||||
// 更新全局用户状态,刷新岗位信息
|
||||
const userStore = useUserStore();
|
||||
await userStore.getUserInfo();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await PositionAPI.deletePosition(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 导出
|
||||
async function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: boolean) {
|
||||
if (selectIds.value.length) {
|
||||
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await PositionAPI.batchAvailablePosition({ ids: selectIds.value, status });
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出字段
|
||||
const exportColumns = [
|
||||
{ prop: 'name', label: '岗位名称' },
|
||||
{ prop: 'order', label: '岗位排序' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'description', label: '描述' },
|
||||
{ prop: 'created_at', label: '创建时间' },
|
||||
{ prop: 'updated_at', label: '更新时间' },
|
||||
];
|
||||
|
||||
// 导入/导出配置(用于导出)
|
||||
const curdContentConfig = {
|
||||
permPrefix: 'system:position',
|
||||
cols: exportColumns as any,
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
if (typeof query.status === 'string') {
|
||||
query.status = query.status === 'true';
|
||||
}
|
||||
query.page_no = 1;
|
||||
query.page_size = 1000;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await PositionAPI.getPositionList(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
onMounted(() => {
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
// 导出字段
|
||||
const exportColumns = [
|
||||
{ prop: 'name', label: '岗位名称' },
|
||||
{ prop: 'order', label: '岗位排序' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'description', label: '描述' },
|
||||
{ prop: 'created_at', label: '创建时间' },
|
||||
{ prop: 'updated_at', label: '更新时间' },
|
||||
];
|
||||
|
||||
// 导入/导出配置(仅用于导出)
|
||||
const curdContentConfig = {
|
||||
permPrefix: 'system:position',
|
||||
cols: exportColumns as any,
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
if (typeof query.status === 'string') {
|
||||
query.status = query.status === 'true';
|
||||
}
|
||||
query.page_no = 1;
|
||||
query.page_size = 1000;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await PositionAPI.getPositionList(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
@@ -0,0 +1,383 @@
|
||||
<!-- 角色授权 -->
|
||||
<template>
|
||||
<!-- 分配权限弹窗 -->
|
||||
<el-drawer v-model="drawerVisible" :title="'【' + props.roleName + '】权限分配'" :size="drawerSize">
|
||||
<el-container>
|
||||
<!-- 数据权限 -->
|
||||
<el-aside >
|
||||
<div class="border-r-1 border-r-[#f0f0f0] b-r-solid h-[100%] p-[20px] box-border">
|
||||
<div class="flex items-center">
|
||||
<div style="display: flex; gap: 10px; ">
|
||||
<div style="width: 10px; background-color: #409eff;"></div>
|
||||
<div>
|
||||
<span style="font-size: 16px;">数据授权</span>
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>授权用户可操作的数据范围</span>
|
||||
</template>
|
||||
<el-icon class="ml-1 inline-block cursor-pointer">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<el-form ref="dataFormRef" :model="permissionState">
|
||||
<el-form-item prop="data_scope">
|
||||
<el-select v-model="permissionState.data_scope">
|
||||
<el-option :key="1" label="仅本人数据权限" :value="1" />
|
||||
<el-option :key="2" label="本部门数据权限" :value="2" />
|
||||
<el-option :key="3" label="本部门及以下数据权限" :value="3" />
|
||||
<el-option :key="4" label="全部数据权限" :value="4" />
|
||||
<el-option :key="5" label="自定义数据权限" :value="5" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div v-if="permissionState.data_scope === 5 && deptTreeData.length" class="mt-5 max-h-[60vh] b-1 b-solid b-[#e5e7eb] p-10px overflow-auto box-border">
|
||||
<el-input v-model="deptFilterText" placeholder="部门名称" />
|
||||
<el-tree
|
||||
ref="deptTreeRef"
|
||||
node-key="value"
|
||||
show-checkbox
|
||||
:data="deptTreeData"
|
||||
:filter-node-method="handleFilter"
|
||||
default-expand-all
|
||||
:highlight-current="true"
|
||||
:check-strictly="!parentChildLinked"
|
||||
style="height: calc(100% - 60px); overflow-y: auto; margin-top: 10px;"
|
||||
@check="deptTreeCheck"
|
||||
>
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-aside>
|
||||
|
||||
<!-- 菜单权限 -->
|
||||
<el-main>
|
||||
<div style="display: flex; gap: 10px; ">
|
||||
<div style="width: 10px; background-color: #409eff;"></div>
|
||||
<div>
|
||||
<span style="font-size: 16px;">菜单授权</span>
|
||||
<el-tooltip placement="right">
|
||||
<template #content>
|
||||
<span>授权用户可操作的菜单权限</span>
|
||||
</template>
|
||||
<el-icon class="ml-1 inline-block cursor-pointer">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex-x-between">
|
||||
<el-input v-model="permFilterText" placeholder="菜单名称" />
|
||||
<div class="flex-center ml-5">
|
||||
<el-button type="primary" size="small" plain @click="togglePermTree">
|
||||
<template #icon>
|
||||
<Switch />
|
||||
</template>
|
||||
{{ isExpanded ? "收缩" : "展开" }}
|
||||
</el-button>
|
||||
<el-checkbox v-model="parentChildLinked" class="ml-5" @change="handleParentChildLinkedChange">
|
||||
父子联动
|
||||
</el-checkbox>
|
||||
|
||||
<el-tooltip placement="bottom">
|
||||
<template #content>
|
||||
如果只需勾选菜单权限,不需要勾选子菜单或者按钮权限,请关闭父子联动
|
||||
</template>
|
||||
<el-icon class="ml-1 color-[--el-color-primary] inline-block cursor-pointer">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 max-h-[65vh] b-1 b-solid b-[#e5e7eb] p-10px overflow-auto box-border">
|
||||
<el-tree
|
||||
ref="permTreeRef"
|
||||
node-key="value"
|
||||
show-checkbox
|
||||
:data="menuTreeData"
|
||||
:filter-node-method="handleFilter"
|
||||
default-expand-all
|
||||
:highlight-current="true"
|
||||
:check-strictly="!parentChildLinked"
|
||||
style="height: calc(100% - 60px); overflow: auto; margin-top: 10px;"
|
||||
@check="menuTreeCheck"
|
||||
>
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleCancel">取 消</el-button>
|
||||
<el-button type="primary" @click.stop="handleDrawerSave">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 添加 props 来接收 dictType
|
||||
const props = defineProps({
|
||||
roleName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
roleId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
import { listToTree, formatTree } from "@/utils/common";
|
||||
import RoleAPI, { permissionDataType, permissionDeptType, permissionMenuType } from "@/api/module_system/role";
|
||||
import DeptAPI from "@/api/module_system/dept";
|
||||
import MenuAPI from "@/api/module_system/menu";
|
||||
import type { TreeInstance } from 'element-plus'
|
||||
import { useAppStore } from "@/store/modules/app.store";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
import { useUserStore } from "@/store";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "800px" : "90%"));
|
||||
const emit = defineEmits(['update:modelValue', 'saved'])
|
||||
|
||||
const permTreeRef = ref<TreeInstance>();
|
||||
const deptTreeRef = ref<TreeInstance>()
|
||||
const deptFilterText = ref("")
|
||||
const permFilterText = ref("");
|
||||
const dataFormRef = ref();
|
||||
const drawerVisible = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
const isExpanded = ref(true);
|
||||
const parentChildLinked = ref(false)
|
||||
const loading = ref<boolean>(false);
|
||||
const deptTreeData = ref<permissionDeptType[]>([]);
|
||||
const menuTreeData = ref<permissionMenuType[]>([]);
|
||||
const permissionState = ref<permissionDataType>({
|
||||
role_ids: [],
|
||||
menu_ids: [],
|
||||
data_scope: 1,
|
||||
dept_ids: []
|
||||
});
|
||||
|
||||
// 初始化方法,用于打开抽屉并加载数据
|
||||
const init = async () => {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// 获取全部部门树
|
||||
const deptResponse = await DeptAPI.getDeptList();
|
||||
deptTreeData.value = formatTree(listToTree(deptResponse.data.data));
|
||||
|
||||
// 获取全部菜单树
|
||||
const menuResponse = await MenuAPI.getMenuList();
|
||||
menuTreeData.value = formatTree(listToTree(menuResponse.data.data));
|
||||
|
||||
// 获取角色详情
|
||||
const roleResponse = await RoleAPI.getRoleDetail(props.roleId);
|
||||
|
||||
// 更新权限状态
|
||||
permissionState.value = {
|
||||
role_ids: [props.roleId],
|
||||
menu_ids: roleResponse.data.data.menus?.map(menu => menu.id) || [],
|
||||
data_scope: roleResponse.data.data.data_scope || 1,
|
||||
dept_ids: roleResponse.data.data.depts?.map(dept => dept.id) || []
|
||||
};
|
||||
|
||||
// 根据保存的权限数据判断是否应该开启父子联动
|
||||
parentChildLinked.value = checkParentChildLinked(permissionState.value.menu_ids, menuTreeData.value);
|
||||
|
||||
// 回显菜单树选中项
|
||||
if (permTreeRef.value) {
|
||||
await permTreeRef.value.setCheckedKeys(permissionState.value.menu_ids);
|
||||
}
|
||||
|
||||
// 修改:增加对 deptTreeRef.value 的存在性判断,并添加日志
|
||||
if (permissionState.value.data_scope === 5 && deptTreeRef.value) {
|
||||
// await 一定不能丢,否则到导致初始化时候deptTreeRef.value 为 undefined
|
||||
await deptTreeRef.value.setCheckedKeys(permissionState.value.dept_ids);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
ElMessage.error('获取权限数据失败: ' + error.message);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
// handleCancel()
|
||||
}
|
||||
}
|
||||
|
||||
// 取消按钮处理函数
|
||||
function handleCancel() {
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
|
||||
// 保存权限分配
|
||||
async function handleDrawerSave () {
|
||||
try {
|
||||
if (props.roleId === 1) {
|
||||
ElMessage.warning('系统默认角色,不可操作');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
|
||||
// 构造提交数据
|
||||
const submitData : permissionDataType = {
|
||||
role_ids: [props.roleId],
|
||||
menu_ids: (permTreeRef.value?.getCheckedKeys() || []).map(key => Number(key)),
|
||||
data_scope: permissionState.value.data_scope,
|
||||
dept_ids: (deptTreeRef.value?.getCheckedKeys() || []).map(key => Number(key))
|
||||
};
|
||||
|
||||
await RoleAPI.setPermission(submitData)
|
||||
|
||||
// 更新全局用户状态,刷新权限信息
|
||||
const userStore = useUserStore();
|
||||
await userStore.getUserInfo();
|
||||
|
||||
drawerVisible.value = false;
|
||||
emit('saved');
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 部门树选择回调
|
||||
const deptTreeCheck = (checkedIds: number[]) => {
|
||||
permissionState.value.dept_ids = checkedIds;
|
||||
}
|
||||
|
||||
// 菜单选择变更回调
|
||||
const menuTreeCheck = (checkedIds: number[]) => {
|
||||
permissionState.value.menu_ids = checkedIds;
|
||||
}
|
||||
|
||||
// 展开/收缩 菜单权限树
|
||||
function togglePermTree() {
|
||||
isExpanded.value = !isExpanded.value;
|
||||
if (permTreeRef.value) {
|
||||
Object.values(permTreeRef.value.store.nodesMap).forEach((node: any) => {
|
||||
if (isExpanded.value) {
|
||||
node.expand();
|
||||
} else {
|
||||
node.collapse();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 部门筛选
|
||||
watch(deptFilterText, (val) => {
|
||||
deptTreeRef.value!.filter(val);
|
||||
})
|
||||
|
||||
// 菜单筛选
|
||||
watch(permFilterText, (val) => {
|
||||
permTreeRef.value!.filter(val);
|
||||
});
|
||||
|
||||
// 树节点过滤
|
||||
function handleFilter(
|
||||
value: string,
|
||||
data: {[key: string]: any;}
|
||||
) {
|
||||
if (!value) return true;
|
||||
return data.label.includes(value);
|
||||
}
|
||||
|
||||
// 检查权限数据是否遵循父子联动模式
|
||||
function checkParentChildLinked(menuIds: number[], menuTreeData: permissionMenuType[]): boolean {
|
||||
if (!menuIds.length || !menuTreeData.length) return false;
|
||||
|
||||
// 创建一个映射来快速查找菜单项
|
||||
const menuMap = new Map<number, permissionMenuType>();
|
||||
const buildMenuMap = (menus: permissionMenuType[]) => {
|
||||
menus.forEach(menu => {
|
||||
menuMap.set(menu.id, menu);
|
||||
if (menu.children) {
|
||||
buildMenuMap(menu.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
buildMenuMap(menuTreeData);
|
||||
|
||||
let hasParentChildConflict = false;
|
||||
|
||||
// 检查每个选中的菜单项
|
||||
for (const menuId of menuIds) {
|
||||
const menu = menuMap.get(menuId);
|
||||
if (!menu) continue;
|
||||
|
||||
// 如果选中了父菜单,检查是否有子菜单未被选中
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const hasUnselectedChildren = menu.children.some(child => !menuIds.includes(child.id));
|
||||
if (hasUnselectedChildren) {
|
||||
hasParentChildConflict = true;
|
||||
break; // 发现冲突,直接返回false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果选中了子菜单,检查父菜单是否也被选中
|
||||
const parentMenu = findParentMenu(menuId, menuTreeData);
|
||||
if (parentMenu && !menuIds.includes(parentMenu.id)) {
|
||||
hasParentChildConflict = true;
|
||||
break; // 发现冲突,直接返回false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有发现父子冲突,说明是父子联动模式
|
||||
return !hasParentChildConflict;
|
||||
}
|
||||
|
||||
// 查找父菜单
|
||||
function findParentMenu(menuId: number, menuTreeData: permissionMenuType[]): permissionMenuType | null {
|
||||
for (const menu of menuTreeData) {
|
||||
if (menu.children) {
|
||||
for (const child of menu.children) {
|
||||
if (child.id === menuId) {
|
||||
return menu;
|
||||
}
|
||||
// 递归查找更深层的父菜单
|
||||
const found = findParentMenu(menuId, [child]);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 父子菜单节点是否联动
|
||||
function handleParentChildLinkedChange(val: any) {
|
||||
parentChildLinked.value = val;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await init();
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,608 @@
|
||||
<!-- 角色管理 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
|
||||
<el-form-item prop="name" label="角色名称">
|
||||
<el-input v-model="queryFormData.name" placeholder="请输入角色名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
|
||||
<el-option value="true" label="启用" />
|
||||
<el-option value="false" label="停用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.creator"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button v-hasPerm="['system:role:query']" type="primary" icon="search" native-type="submit">查询</el-button>
|
||||
<el-button v-hasPerm="['system:role:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="角色管理维护系统的角色和权限。">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
角色管理列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:role:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:role:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['system:role:patch']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">
|
||||
更多
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu v-hasPerm="['system:role:filter']">
|
||||
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
|
||||
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button v-hasPerm="['system:role:export']" type="warning" icon="download" circle @click="handleOpenExportsModal" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button v-hasPerm="['system:role:refresh']" type="primary" icon="refresh" circle @click="handleRefresh" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-popover placement="bottom" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="danger" icon="operation" circle></el-button>
|
||||
</template>
|
||||
<el-scrollbar max-height="350px">
|
||||
<template v-for="column in tableColumns" :key="column.prop">
|
||||
<el-checkbox v-if="column.prop" v-model="column.show" :label="column.label" />
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-popover>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'selection')?.show" type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'index')?.show" type="index" fixed label="序号" width="60" >
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'name')?.show" key="name" label="角色名称" prop="name" min-width="100" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'code')?.show" key="code" label="角色编码" prop="code" min-width="100" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'data_scope')?.show" key="data_scope" label="数据权限" prop="data_scope" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.data_scope === 1" type="primary">仅本人数据权限</el-tag>
|
||||
<el-tag v-else-if="scope.row.data_scope === 2" type="info">本部门数据权限</el-tag>
|
||||
<el-tag v-else-if="scope.row.data_scope === 3" type="warning">本部门及以下数据权限</el-tag>
|
||||
<el-tag v-else-if="scope.row.data_scope === 4" type="success">全部数据权限</el-tag>
|
||||
<el-tag v-else type="danger" >自定义数据权限</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'order')?.show" key="order" label="排序" prop="order" min-width="80" show-overflow-tooltip />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'status')?.show" key="status" label="状态" prop="status" min-width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
|
||||
{{ scope.row.status === true ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'description')?.show" key="description" label="描述" prop="description" min-width="100" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'created_at')?.show" key="created_at" label="创建时间" prop="created_at" min-width="200" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'updated_at')?.show" key="updated_at" label="更新时间" prop="updated_at" min-width="200" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'creator')?.show" key="creator" label="创建人" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.creator?.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="280">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPerm="['system:role:permission']"
|
||||
type="warning"
|
||||
size="small"
|
||||
link
|
||||
icon="position"
|
||||
:disabled="scope.row.id === 1"
|
||||
@click="scope.row.id === 1 ? ElMessage.warning('系统默认角色,不可操作') : handleOpenAssignPermDialog(scope.row.id, scope.row.name)"
|
||||
>分配权限</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['system:role:detail']"
|
||||
type="info"
|
||||
size="small"
|
||||
link
|
||||
icon="document"
|
||||
@click="handleOpenDialog('detail', scope.row.id)"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['system:role:update']"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
icon="edit"
|
||||
:disabled="scope.row.id === 1"
|
||||
@click="scope.row.id === 1 ? ElMessage.warning('系统默认角色,不可操作') : handleOpenDialog('update', scope.row.id)"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['system:role:delete']"
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
icon="delete"
|
||||
:disabled="scope.row.id === 1"
|
||||
@click="scope.row.id === 1 ? ElMessage.warning('系统默认角色,不可操作') : handleDelete([scope.row.id])"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 角色表单弹窗 -->
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="角色名称" :span="2">{{ detailFormData.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="排序" :span="2">
|
||||
{{ detailFormData.order }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色编码" :span="2">{{ detailFormData.code }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数据权限" :span="2">
|
||||
<el-tag v-if="detailFormData.data_scope === 1" type="primary">仅本人数据权限</el-tag>
|
||||
<el-tag v-else-if="detailFormData.data_scope === 2" type="info">本部门数据权限</el-tag>
|
||||
<el-tag v-else-if="detailFormData.data_scope === 3" type="warning">本部门及以下数据权限</el-tag>
|
||||
<el-tag v-else-if="detailFormData.data_scope === 4" type="success">全部数据权限</el-tag>
|
||||
<el-tag v-else type="danger" >自定义数据权限</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag :type="detailFormData.status ? 'success' : 'danger'">
|
||||
{{ detailFormData.status ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人" :span="2">{{ detailFormData.creator?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ detailFormData.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ detailFormData.updated_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="4">{{ detailFormData.description }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入角色名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="order">
|
||||
<el-input-number v-model="formData.order" controls-position="right" :min="0" style="width: 100px" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入角色编码" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="true">启用</el-radio>
|
||||
<el-radio :value="false">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="formData.description" :rows="4" :maxlength="100" show-word-limit type="textarea" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleCloseDialog">取 消</el-button>
|
||||
<el-button v-hasPerm="['system:role:submit']" type="primary" @click="handleSubmit">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<PermissonDrawer v-if="drawerVisible" v-model="drawerVisible" :role-name="checkedRole.name" :role-id="checkedRole.id" @saved="handleRefresh" />
|
||||
|
||||
<!-- 导出弹窗 -->
|
||||
<ExportModal
|
||||
v-model="exportsDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
:query-params="queryFormData"
|
||||
:page-data="pageTableData"
|
||||
:selection-data="selectionRows"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "Role",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import RoleAPI, { RoleTable, RoleForm, TablePageQuery } from "@/api/module_system/role";
|
||||
import { useUserStore } from "@/store";
|
||||
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import { QuestionFilled, ArrowUp, ArrowDown } from "@element-plus/icons-vue";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<RoleTable[]>([]);
|
||||
|
||||
// 抽屉显隐
|
||||
const drawerVisible = ref(false);
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = ref([
|
||||
{ prop: 'selection', label: '选择框', show: true },
|
||||
{ prop: 'index', label: '序号', show: true },
|
||||
{ prop: 'name', label: '角色名称', show: true },
|
||||
{ prop: 'data_scope', label: '数据权限', show: true },
|
||||
{ prop: 'order', label: '排序', show: true },
|
||||
{ prop: 'code', label: '角色编码', show: true },
|
||||
{ prop: 'status', label: '状态', show: true },
|
||||
{ prop: 'description', label: '描述', show: true },
|
||||
{ prop: 'created_at', label: '创建时间', show: true },
|
||||
{ prop: 'updated_at', label: '更新时间', show: true },
|
||||
{ prop: 'creator', label: '创建人', show: true },
|
||||
{ prop: 'operation', label: '操作', show: true }
|
||||
])
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<RoleTable>({} as RoleTable);
|
||||
|
||||
// 选中的角色
|
||||
const checkedRole = ref<RoleTable>({} as RoleTable);
|
||||
|
||||
const queryFormData = reactive<TablePageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
name: '',
|
||||
status: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
// 创建人
|
||||
creator: undefined,
|
||||
});
|
||||
|
||||
// 新增、编辑表单
|
||||
const formData = reactive<RoleForm>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
order: 1,
|
||||
code: undefined,
|
||||
status: true,
|
||||
description: undefined,
|
||||
});
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'create' as 'create' | 'update' | 'detail',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
|
||||
order: [{ required: true, message: "请输入角色排序", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
|
||||
});
|
||||
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await RoleAPI.getRoleList(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh () {
|
||||
await loadingData();
|
||||
};
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
// 额外清空日期范围与时间查询参数
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: RoleForm = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
order: 1,
|
||||
code: undefined,
|
||||
status: true,
|
||||
description: undefined,
|
||||
}
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开系统配置弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await RoleAPI.getRoleDetail(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = "角色详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === 'update') {
|
||||
dialogVisible.title = "修改角色";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增角色";
|
||||
formData.id = undefined;
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 新增、编辑弹窗处理
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
try {
|
||||
if (id) {
|
||||
await RoleAPI.updateRole(id, { id, ...formData })
|
||||
} else {
|
||||
await RoleAPI.createRole(formData)
|
||||
}
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleResetQuery();
|
||||
// 更新全局用户状态,刷新权限信息
|
||||
const userStore = useUserStore();
|
||||
await userStore.getUserInfo();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await RoleAPI.deleteRole(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 导出已改为通过导出弹窗 ExportModal 统一处理
|
||||
function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: boolean) {
|
||||
if (selectIds.value.length) {
|
||||
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await RoleAPI.batchAvailableRole({ ids: selectIds.value, status });
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 打开分配菜单权限弹窗
|
||||
async function handleOpenAssignPermDialog(roleId: number, roleName: string) {
|
||||
if (roleId) {
|
||||
drawerVisible.value = true;
|
||||
checkedRole.value.id = roleId;
|
||||
checkedRole.value.name = roleName;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出弹窗显示状态
|
||||
const exportsDialogVisible = ref(false);
|
||||
const selectionRows = ref<RoleTable[]>([]);
|
||||
// 导出字段
|
||||
const exportColumns = [
|
||||
{ prop: 'name', label: '角色名称' },
|
||||
{ prop: 'code', label: '角色编码' },
|
||||
{ prop: 'data_scope', label: '数据权限' },
|
||||
{ prop: 'order', label: '排序' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'description', label: '描述' },
|
||||
{ prop: 'created_at', label: '创建时间' },
|
||||
{ prop: 'updated_at', label: '更新时间' },
|
||||
];
|
||||
|
||||
// 导入/导出配置
|
||||
const curdContentConfig = {
|
||||
permPrefix: 'system:role',
|
||||
cols: exportColumns as any,
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
if (typeof query.status === 'string') {
|
||||
query.status = query.status === 'true';
|
||||
}
|
||||
query.page_no = 1;
|
||||
query.page_size = 1000;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await RoleAPI.getRoleList(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
onMounted(() => {
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<!-- 部门树 -->
|
||||
<template>
|
||||
<el-card shadow="hover">
|
||||
<el-input v-model="deptName" placeholder="部门名称">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-tree
|
||||
ref="deptTreeRef"
|
||||
class="mt-2"
|
||||
:data="deptOptions"
|
||||
:props="{ children: 'children', label: 'label', disabled: 'disabled' }"
|
||||
:expand-on-click-node="false"
|
||||
:filter-node-method="handleFilter"
|
||||
default-expand-all
|
||||
@node-click="handleNodeClick" >
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DeptAPI, { DeptPageQuery } from "@/api/module_system/dept";
|
||||
import { formatTree } from "@/utils/common";
|
||||
import type { FilterNodeMethodFunction, TreeInstance } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const deptOptions = ref<OptionType[]>([]); // 部门列表
|
||||
const deptTreeRef = ref<TreeInstance>(); // 部门树
|
||||
const deptName = ref(); // 部门名称
|
||||
|
||||
const emits = defineEmits(["node-click", "update:modelValue"]);
|
||||
|
||||
const deptId = useVModel(props, "modelValue", emits);
|
||||
|
||||
watch(deptName, (val) => {
|
||||
deptTreeRef.value!.filter(val)
|
||||
})
|
||||
|
||||
interface Tree {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门筛选
|
||||
*/
|
||||
const handleFilter: FilterNodeMethodFunction = (value: string, data: Tree) => {
|
||||
if (!value) return true;
|
||||
return data.label.includes(value)
|
||||
}
|
||||
|
||||
/** 部门树节点 Click */
|
||||
function handleNodeClick(data: { [key: string]: any }) {
|
||||
deptId.value = data.value;
|
||||
emits("node-click");
|
||||
}
|
||||
|
||||
const queryFormData = reactive<DeptPageQuery>({
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
});
|
||||
|
||||
const loading = ref(true);
|
||||
|
||||
onBeforeMount(() => {
|
||||
loading.value = true;
|
||||
DeptAPI.getDeptList(queryFormData).then((response) => {
|
||||
deptOptions.value = formatTree(response.data.data);
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,127 @@
|
||||
<!-- 列表选择器 -->
|
||||
<template>
|
||||
<table-select :text="text" :select-config="selectConfig" @confirm-click="handleConfirm" @clear-click="handleClearSelection">
|
||||
<template #status="scope">
|
||||
<el-tag :type="scope.row[scope.prop] === true ? 'success' : 'danger'">
|
||||
{{ scope.row[scope.prop] === true ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</table-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ISelectConfig } from "@/components/TableSelect/index.vue";
|
||||
import UserAPI from "@/api/module_system/user";
|
||||
|
||||
// 父组件双向绑定的值(选中用户ID)
|
||||
const props = defineProps<{ modelValue?: number }>();
|
||||
|
||||
// 事件向上派发,支持父组件监听和 v-model(仅回传选中用户ID)
|
||||
const emit = defineEmits<{
|
||||
confirmClick: [data: IUser[]];
|
||||
"update:modelValue": [val: number | undefined];
|
||||
}>();
|
||||
|
||||
const selectConfig: ISelectConfig = {
|
||||
pk: "id",
|
||||
width: "167.5px", // 与搜索表单其他输入宽度一致
|
||||
placeholder: "请选择用户",
|
||||
popover: {
|
||||
width: 720, // 弹出层宽度,提升可用性(约 720px)
|
||||
},
|
||||
formItems: [
|
||||
{
|
||||
type: "select",
|
||||
label: "状态",
|
||||
prop: "status",
|
||||
initialValue: true,
|
||||
attrs: {
|
||||
placeholder: "全部",
|
||||
clearable: true,
|
||||
style: {
|
||||
width: "140px",
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{ label: "启用", value: true },
|
||||
{ label: "停用", value: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
indexAction(params) {
|
||||
// 映射查询参数到后端接口
|
||||
const query: any = { ...params };
|
||||
// 清理空字符串/空值,避免后端 422
|
||||
Object.keys(query).forEach((k) => {
|
||||
const v = query[k];
|
||||
if (v === "" || v === null || v === undefined) {
|
||||
delete query[k];
|
||||
}
|
||||
});
|
||||
// 规范化状态为布尔值
|
||||
if (typeof query.status === "string") {
|
||||
if (query.status === "true") query.status = true;
|
||||
else if (query.status === "false") query.status = false;
|
||||
}
|
||||
// 请求用户分页列表并适配 TableSelect 需要的结构
|
||||
return UserAPI.getUserList(query).then((res: any) => {
|
||||
return {
|
||||
total: res.data.data.total,
|
||||
list: res.data.data.items,
|
||||
};
|
||||
});
|
||||
},
|
||||
tableColumns: [
|
||||
{ type: "selection", width: 50, align: "center" },
|
||||
{ label: "编号", align: "center", prop: "id", width: 100 },
|
||||
{ label: "账号", align: "center", prop: "username" },
|
||||
{ label: "用户名", align: "center", prop: "name", width: 120 },
|
||||
{
|
||||
label: "状态",
|
||||
align: "center",
|
||||
prop: "status",
|
||||
templet: "custom",
|
||||
slotName: "status",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface IUser {
|
||||
id: number;
|
||||
username: string;
|
||||
name: string;
|
||||
mobile?: string;
|
||||
gender?: string;
|
||||
avatar?: string;
|
||||
email?: string | null;
|
||||
status?: boolean;
|
||||
dept_name?: string;
|
||||
role_names?: string[];
|
||||
created_at?: string;
|
||||
}
|
||||
const selectedUser = ref<IUser>();
|
||||
function handleConfirm(data: IUser[]) {
|
||||
selectedUser.value = data[0];
|
||||
const id = selectedUser.value?.id;
|
||||
emit("update:modelValue", id);
|
||||
emit("confirmClick", data);
|
||||
}
|
||||
function handleClearSelection() {
|
||||
selectedUser.value = undefined;
|
||||
emit("update:modelValue", undefined);
|
||||
}
|
||||
|
||||
// 当父组件重置 v-model(modelValue)为 undefined 时,清空本地显示
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val === undefined || val === null) {
|
||||
selectedUser.value = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const text = computed(() => {
|
||||
return selectedUser.value ? `${selectedUser.value.username} - ${selectedUser.value.name}` : "";
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,806 @@
|
||||
<!-- 用户管理 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-row :gutter="12">
|
||||
<!-- 部门树 -->
|
||||
<el-col :lg="4" :xs="24">
|
||||
<DeptTree v-model="queryFormData.dept_id" class="h-full" @node-click="handleQuery" />
|
||||
</el-col>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<el-col :lg="20" :xs="24">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
|
||||
<el-form-item prop="username" label="账号">
|
||||
<el-input v-model="queryFormData.username" placeholder="请输入账号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="name" label="用户名">
|
||||
<el-input v-model="queryFormData.name" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
|
||||
<el-option value="true" label="启用" />
|
||||
<el-option value="false" label="停用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.creator"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button v-hasPerm="['system:user:query']" type="primary" icon="search" native-type="submit">查询</el-button>
|
||||
<el-button v-hasPerm="['system:user:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="用户管理系统用户">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
用户列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:user:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPerm="['system:user:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['system:user:patch']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">
|
||||
更多
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu v-hasPerm="['system:user:filter']">
|
||||
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
|
||||
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导入">
|
||||
<el-button v-hasPerm="['system:user:import']" type="info" icon="upload" circle @click="handleOpenImportDialog" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button v-hasPerm="['system:user:export']" type="warning" icon="download" circle @click="handleOpenExportsModal" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button v-hasPerm="['system:user:refresh']" type="default" icon="refresh" circle @click="handleRefresh"/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column type="index" fixed label="序号" align="center" min-width="60" >
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="头像" prop="avatar" min-width="80" align="center">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.avatar">
|
||||
<el-avatar size="small" :src="scope.row.avatar" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-avatar size="small" icon="UserFilled" />
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账号" prop="username" min-width="100" />
|
||||
<el-table-column label="用户名" prop="name" min-width="100" />
|
||||
<el-table-column label="状态" prop="status" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
|
||||
{{ scope.row.status === true ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否超管" prop="is_superuser" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.is_superuser ? 'success' : 'info'">
|
||||
{{ scope.row.is_superuser ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="性别" prop="gender" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.gender ==='0'" type="success">男</el-tag>
|
||||
<el-tag v-else-if="scope.row.gender ==='1'" type="warning">女</el-tag>
|
||||
<el-tag v-else type="info">未知</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="手机号" prop="mobile" min-width="160" />
|
||||
<el-table-column label="邮箱" prop="email" min-width="160" />
|
||||
<el-table-column label="创建时间" prop="created_at" min-width="200" />
|
||||
<el-table-column label="更新时间" prop="updated_at" min-width="200" />
|
||||
<el-table-column label="创建人" prop="creator" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ scope.row.creator?.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" align="center" min-width="280">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPerm="['system:user:update']"
|
||||
type="warning"
|
||||
icon="RefreshLeft"
|
||||
size="small"
|
||||
link
|
||||
:disabled="scope.row.is_superuser === true"
|
||||
@click="scope.row.is_superuser === true ? ElMessage.warning('系统超管角色,不可操作') : hancleResetPassword(scope.row)"
|
||||
>重置密码</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['system:user:detail']"
|
||||
type="info"
|
||||
size="small"
|
||||
link
|
||||
icon="document"
|
||||
@click="handleOpenDialog('detail', scope.row.id)"
|
||||
>详情</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['system:user:update']"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
icon="edit"
|
||||
:disabled="scope.row.is_superuser === true"
|
||||
@click="scope.row.is_superuser === true ? ElMessage.warning('系统超管角色,不可操作') : handleOpenDialog('update', scope.row.id)"
|
||||
>编辑</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['system:user:delete']"
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
icon="delete"
|
||||
:disabled="scope.row.is_superuser === true"
|
||||
@click="scope.row.is_superuser === true ? ElMessage.warning('系统超管角色,不可操作') : handleDelete([scope.row.id])"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
|
||||
</template>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-drawer v-model="dialogVisible.visible" :title="dialogVisible.title" append-to-body :size="drawerSize" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="编号" :span="2">{{ detailFormData.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="头像" :span="2">
|
||||
<template v-if="detailFormData.avatar">
|
||||
<el-avatar :src="detailFormData.avatar" size="small"></el-avatar>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-avatar icon="UserFilled" size="small"></el-avatar>
|
||||
</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="账号" :span="2">{{ detailFormData.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户名" :span="2">{{ detailFormData.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别" :span="2">
|
||||
<el-tag v-if="detailFormData.gender ==='0'" type="success">男</el-tag>
|
||||
<el-tag v-else-if="detailFormData.gender ==='1'" type="warning">女</el-tag>
|
||||
<el-tag v-else type="info">未知</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="部门" :span="2">{{ detailFormData.dept ? detailFormData.dept.name : '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="角色" :span="2">{{ detailFormData.roles ? detailFormData.roles.map(item => item.name).join('、') : '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="岗位" :span="2">{{ detailFormData.positions ? detailFormData.positions.map(item => item.name).join('、') : '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱" :span="2">{{ detailFormData.email }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号" :span="2">{{ detailFormData.mobile }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否超管" :span="2">
|
||||
<el-tag :type="detailFormData.is_superuser ? 'success' : 'info'">
|
||||
{{ detailFormData.is_superuser ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag :type="detailFormData.status ? 'success' : 'danger'">
|
||||
{{ detailFormData.status ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上次登录时间" :span="2">{{ detailFormData.last_login }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人" :span="2">{{ detailFormData.creator?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ detailFormData.created_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ detailFormData.updated_at }}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="4">{{ detailFormData.description }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
|
||||
<el-form-item label="账号" prop="username">
|
||||
<el-input v-model="formData.username" :disabled="!!formData.id" placeholder="请输入账号" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="用户名" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="性别" prop="gender">
|
||||
<el-select v-model="formData.gender" placeholder="请选择性别">
|
||||
<el-option label="男" value="0" />
|
||||
<el-option label="女" value="1" />
|
||||
<el-option label="未知" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="手机号" prop="mobile">
|
||||
<el-input v-model="formData.mobile" placeholder="请输入手机号码" maxlength="11" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="formData.email" placeholder="请输入邮箱" maxlength="50" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="部门" prop="dept_id">
|
||||
<el-tree-select v-model="formData.dept_id" placeholder="请选择上级部门" :data="deptOptions" :props="{ children: 'children', label: 'label', disabled: 'disabled' }" filterable check-strictly :render-after-expand="false" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色" prop="role_ids">
|
||||
<el-select v-model="formData.role_ids" multiple placeholder="请选择角色">
|
||||
<el-option v-for="item in roleOptions" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="岗位" prop="position_ids">
|
||||
<el-select v-model="formData.position_ids" multiple placeholder="请选择岗位">
|
||||
<el-option v-for="item in positionOptions" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="formData.password" :readonly="!!formData.id" placeholder="请输入密码" type="password" show-password clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否超管" prop="is_superuser">
|
||||
<el-switch v-model="formData.is_superuser" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="true">启用</el-radio>
|
||||
<el-radio :value="false">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="formData.description" :rows="4" :maxlength="100" show-word-limit type="textarea" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
|
||||
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button v-if="dialogVisible.type === 'create' || dialogVisible.type === 'update'" v-hasPerm="['system:user:create']" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else v-hasPerm="['system:user:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<ImportModal
|
||||
v-model="importDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
@upload="handleUpload"
|
||||
/>
|
||||
|
||||
<!-- 导出弹窗 -->
|
||||
<ExportModal
|
||||
v-model="exportsDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
:query-params="queryFormData"
|
||||
:page-data="pageTableData"
|
||||
:selection-data="selectionRows"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "User",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { useAppStore } from "@/store/modules/app.store";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
import { ResultEnum } from "@/enums/api/result.enum";
|
||||
|
||||
import UserAPI, { type UserForm, type UserInfo, type UserPageQuery } from "@/api/module_system/user";
|
||||
import { formatTree } from "@/utils/common";
|
||||
import PositionAPI from "@/api/module_system/position";
|
||||
import DeptAPI from "@/api/module_system/dept";
|
||||
import RoleAPI from "@/api/module_system/role";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
import DeptTree from "./components/DeptTree.vue";
|
||||
import UserTableSelect from "./components/UserTableSelect.vue";
|
||||
import { useUserStore } from "@/store";
|
||||
import ImportModal from "@/components/CURD/ImportModal.vue";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import { QuestionFilled, ArrowUp, ArrowDown } from "@element-plus/icons-vue";
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "450px" : "90%"));
|
||||
const selectionRows = ref<UserInfo[]>([]);
|
||||
// 选中的用户ID
|
||||
const selectIds = ref<number[]>([]);
|
||||
// 部门下拉数据源
|
||||
const deptOptions = ref<OptionType[]>();
|
||||
// 角色下拉数据源
|
||||
const roleOptions = ref<Array<{ value: number; label: string; disabled?: boolean }>>();
|
||||
// 岗位下拉数据源
|
||||
const positionOptions = ref<Array<{ value: number; label: string; disabled?: boolean }>>();
|
||||
// 导入弹窗显示状态
|
||||
const importDialogVisible = ref(false);
|
||||
// 导出弹窗显示状态
|
||||
const exportsDialogVisible = ref(false);
|
||||
// 分页表单
|
||||
const pageTableData = ref<UserInfo[]>([]);
|
||||
// 详情表单
|
||||
const detailFormData = ref<UserInfo>({});
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<UserPageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
username: undefined,
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
dept_id: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
// 创建人
|
||||
creator: undefined,
|
||||
});
|
||||
|
||||
// 表单
|
||||
const formData = reactive<UserForm>({
|
||||
id: undefined,
|
||||
username: undefined,
|
||||
name: undefined,
|
||||
dept_id: undefined,
|
||||
dept_name: undefined,
|
||||
role_ids: undefined,
|
||||
role_names: undefined,
|
||||
position_ids: undefined,
|
||||
position_names: undefined,
|
||||
password: undefined,
|
||||
gender: undefined,
|
||||
email: undefined,
|
||||
mobile: undefined,
|
||||
is_superuser: false, //默认不是超管
|
||||
status: true,
|
||||
description: undefined,
|
||||
})
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'create' as 'create' | 'update' | 'detail',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
|
||||
name: [{ required: true, message: "请输入用户名", trigger: "blur" }],
|
||||
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
|
||||
gender: [{ required: true, message: "请选择性别", trigger: "blur" }],
|
||||
email: [
|
||||
{
|
||||
pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
|
||||
message: "请输入正确的邮箱地址",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
mobile: [
|
||||
{
|
||||
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
|
||||
message: "请输入正确的手机号码",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
is_superuser: [{ required: true, message: "请选择是否超管", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 仅用于导出字段的列(排除非数据列及嵌套对象列)
|
||||
const exportColumns = [
|
||||
{ prop: 'username', label: '账号' },
|
||||
{ prop: 'name', label: '名称' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'gender', label: '性别' },
|
||||
{ prop: 'email', label: '邮箱' },
|
||||
{ prop: 'mobile', label: '手机号' },
|
||||
{ prop: 'is_superuser', label: '是否超管' },
|
||||
{ prop: 'description', label: '描述' },
|
||||
{ prop: 'created_at', label: '创建时间' },
|
||||
{ prop: 'updated_at', label: '更新时间' },
|
||||
];
|
||||
|
||||
// 导入/导出配置
|
||||
const curdContentConfig = {
|
||||
permPrefix: 'system:user',
|
||||
cols: exportColumns as any,
|
||||
importTemplate: () => UserAPI.downloadTemplate(),
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
if (typeof query.status === 'string') {
|
||||
query.status = query.status === 'true';
|
||||
}
|
||||
query.page_no = 1;
|
||||
query.page_size = 9999;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await UserAPI.getUserList(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh () {
|
||||
await loadingData();
|
||||
};
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await UserAPI.getUserList(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
// 重置表单字段
|
||||
queryFormRef.value.resetFields();
|
||||
// 额外清空不在 model 内的扩展查询项(如日期范围)
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
// 清空部门并重置页码
|
||||
queryFormData.dept_id = undefined;
|
||||
// 清空创建人
|
||||
queryFormData.creator = undefined;
|
||||
queryFormData.page_no = 1;
|
||||
// 重新加载数据
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: UserForm = {
|
||||
id: undefined,
|
||||
username: undefined,
|
||||
name: undefined,
|
||||
dept_id: undefined,
|
||||
dept_name: undefined,
|
||||
role_ids: undefined,
|
||||
role_names: undefined,
|
||||
position_ids: undefined,
|
||||
position_names: undefined,
|
||||
password: undefined,
|
||||
gender: undefined,
|
||||
email: undefined,
|
||||
mobile: undefined,
|
||||
is_superuser: false, //默认不是超管
|
||||
status: true,
|
||||
description: undefined,
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 选中项发生变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
function hancleResetPassword(row: UserInfo) {
|
||||
ElMessageBox.prompt("请输入用户【" + row.username + "】的新密码", "重置密码", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
}).then(
|
||||
async ({ value }) => {
|
||||
if (!value || value.length < 6) {
|
||||
ElMessage.warning("密码至少需要6位字符,请重新输入");
|
||||
return false;
|
||||
}
|
||||
await UserAPI.resetUserPassword({id: row.id!, password: value})
|
||||
},
|
||||
() => {
|
||||
ElMessageBox.close();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await UserAPI.getUserDetail(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = "用户详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === 'update') {
|
||||
dialogVisible.title = "修改用户";
|
||||
Object.assign(formData, response.data.data);
|
||||
// 确保角色和岗位ID正确设置
|
||||
formData.role_ids = (response.data.data.roles || []).map(item => item.id as number);
|
||||
formData.position_ids = (response.data.data.positions || []).map(item => item.id as number);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增用户";
|
||||
formData.id = undefined;
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
await nextTick();
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
|
||||
// 获取部门树
|
||||
const deptResponse = await DeptAPI.getDeptList(queryFormData);
|
||||
const treeData = deptResponse.data.data;
|
||||
deptOptions.value = formatTree(treeData);
|
||||
|
||||
// 获取角色列表
|
||||
const roleResponse = await RoleAPI.getRoleList();
|
||||
roleOptions.value = roleResponse.data.data.items
|
||||
.filter(item => item.id !== undefined && item.name !== undefined)
|
||||
.map(item => ({
|
||||
value: item.id as number,
|
||||
label: item.name as string,
|
||||
disabled: item.status === false || String(item.status) === 'false'
|
||||
}))
|
||||
.filter(opt => !opt.disabled);
|
||||
|
||||
// 获取岗位列表
|
||||
const positionResponse = await PositionAPI.getPositionList();
|
||||
positionOptions.value = positionResponse.data.data.items
|
||||
.filter(item => item.id !== undefined && item.name !== undefined)
|
||||
.map(item => ({
|
||||
value: item.id as number,
|
||||
label: item.name as string,
|
||||
disabled: item.status === false || String(item.status) === 'false'
|
||||
}))
|
||||
.filter(opt => !opt.disabled);
|
||||
}
|
||||
|
||||
// 提交表单(防抖)
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
try {
|
||||
if (id) {
|
||||
await UserAPI.updateUser(id, { id, ...formData })
|
||||
} else {
|
||||
await UserAPI.createUser(formData)
|
||||
}
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleResetQuery();
|
||||
// 如果当前编辑的是登录用户,更新全局用户状态
|
||||
const userStore = useUserStore();
|
||||
if (id === userStore.basicInfo.id) {
|
||||
await userStore.getUserInfo();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 导出已改为通过导出弹窗 ExportModal 统一处理
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await UserAPI.deleteUser(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: boolean) {
|
||||
if (selectIds.value.length) {
|
||||
ElMessageBox.confirm("确认启用或停用该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await UserAPI.batchAvailableUser({ ids: selectIds.value, status });
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 打开导入弹窗
|
||||
function handleOpenImportDialog() {
|
||||
importDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 打开导出弹窗
|
||||
function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['import-success']);
|
||||
|
||||
// 上传文件
|
||||
const handleUpload = async (formData: FormData) => {
|
||||
try {
|
||||
const response = await UserAPI.importUser(formData);
|
||||
if (response.data.code === ResultEnum.SUCCESS) {
|
||||
ElMessage.success(`${response.data.msg},${response.data.data}`);
|
||||
importDialogVisible.value = false;
|
||||
await handleQuery();
|
||||
emit('import-success');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
ElMessage.error('上传失败:' + error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
handleQuery();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user