refactor: 权限标识修改同步调整
This commit is contained in:
@@ -90,12 +90,11 @@ export interface AiExecuteResponse {
|
||||
|
||||
export interface AiCommandRecordPageQuery extends PageQuery {
|
||||
keywords?: string;
|
||||
executeStatus?: string;
|
||||
parseSuccess?: boolean;
|
||||
executeStatus?: number;
|
||||
parseStatus?: number;
|
||||
userId?: number;
|
||||
isDangerous?: boolean;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
aiProvider?: string;
|
||||
aiModel?: string;
|
||||
functionName?: string;
|
||||
createTime?: [string, string];
|
||||
}
|
||||
@@ -105,33 +104,23 @@ export interface AiCommandRecordVO {
|
||||
userId: number;
|
||||
username: string;
|
||||
originalCommand: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
parseSuccess?: boolean;
|
||||
aiProvider?: string;
|
||||
aiModel?: string;
|
||||
parseStatus?: number;
|
||||
functionCalls?: string;
|
||||
explanation?: string;
|
||||
confidence?: number;
|
||||
parseErrorMessage?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
parseTime?: number;
|
||||
parseDurationMs?: number;
|
||||
functionName?: string;
|
||||
functionArguments?: string;
|
||||
executeStatus?: string;
|
||||
executeResult?: string;
|
||||
executeStatus?: number;
|
||||
executeErrorMessage?: string;
|
||||
affectedRows?: number;
|
||||
isDangerous?: boolean;
|
||||
requiresConfirmation?: boolean;
|
||||
userConfirmed?: boolean;
|
||||
executionTime?: number;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
currentRoute?: string;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,9 +169,9 @@ class AiCommandApi {
|
||||
/**
|
||||
* 撤销命令执行(如果支持)
|
||||
*/
|
||||
static rollbackCommand(recordId: string) {
|
||||
static rollbackCommand(logId: string) {
|
||||
return request({
|
||||
url: `/api/v1/ai/command/rollback/${recordId}`,
|
||||
url: `/api/v1/ai/command/rollback/${logId}`,
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
68
src/api/system/tenant-api.ts
Normal file
68
src/api/system/tenant-api.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
const TENANT_BASE_URL = "/api/v1/tenant";
|
||||
|
||||
/**
|
||||
* 租户信息
|
||||
*/
|
||||
export interface TenantInfo {
|
||||
/** 租户ID */
|
||||
id: number;
|
||||
/** 租户名称 */
|
||||
name: string;
|
||||
/** 租户编码 */
|
||||
code?: string;
|
||||
/** 租户状态(1-正常 0-禁用) */
|
||||
status?: number;
|
||||
/** 联系人姓名 */
|
||||
contactName?: string;
|
||||
/** 联系人电话 */
|
||||
contactPhone?: string;
|
||||
/** 联系人邮箱 */
|
||||
contactEmail?: string;
|
||||
/** 租户域名 */
|
||||
domain?: string;
|
||||
/** 租户Logo */
|
||||
logo?: string;
|
||||
/** 是否默认租户 */
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户 API
|
||||
*/
|
||||
const TenantAPI = {
|
||||
/**
|
||||
* 获取当前用户的租户列表
|
||||
*/
|
||||
getTenantList() {
|
||||
return request<any, TenantInfo[]>({
|
||||
url: `${TENANT_BASE_URL}/list`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前租户信息
|
||||
*/
|
||||
getCurrentTenant() {
|
||||
return request<any, TenantInfo>({
|
||||
url: `${TENANT_BASE_URL}/current`,
|
||||
method: "get",
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换租户
|
||||
*
|
||||
* @param tenantId 目标租户ID
|
||||
*/
|
||||
switchTenant(tenantId: number) {
|
||||
return request<any, TenantInfo>({
|
||||
url: `${TENANT_BASE_URL}/switch/${tenantId}`,
|
||||
method: "post",
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default TenantAPI;
|
||||
@@ -15,43 +15,43 @@
|
||||
<stop offset="60%" stop-color="#e3edff" />
|
||||
<stop offset="100%" stop-color="#d6e7ff" />
|
||||
</linearGradient>
|
||||
|
||||
|
||||
<linearGradient id="bgDark" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0b1324" />
|
||||
<stop offset="60%" stop-color="#162135" />
|
||||
<stop offset="100%" stop-color="#1e2c44" />
|
||||
</linearGradient>
|
||||
|
||||
|
||||
<radialGradient id="glowLight" cx="20%" cy="15%" r="60%">
|
||||
<stop offset="0%" stop-color="rgba(64,128,255,0.35)" />
|
||||
<stop offset="40%" stop-color="rgba(64,128,255,0.18)" />
|
||||
<stop offset="100%" stop-color="rgba(64,128,255,0)" />
|
||||
</radialGradient>
|
||||
|
||||
|
||||
<radialGradient id="glowDark" cx="20%" cy="15%" r="60%">
|
||||
<stop offset="0%" stop-color="rgba(98,142,255,0.4)" />
|
||||
<stop offset="50%" stop-color="rgba(98,142,255,0.18)" />
|
||||
<stop offset="100%" stop-color="rgba(98,142,255,0)" />
|
||||
</radialGradient>
|
||||
|
||||
|
||||
<radialGradient id="glowSecondary" cx="80%" cy="70%" r="55%">
|
||||
<stop offset="0%" stop-color="rgba(22,93,255,0.3)" />
|
||||
<stop offset="50%" stop-color="rgba(22,93,255,0.12)" />
|
||||
<stop offset="100%" stop-color="rgba(22,93,255,0)" />
|
||||
</radialGradient>
|
||||
|
||||
|
||||
<linearGradient id="meshLight" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="rgba(255,255,255,0.6)" />
|
||||
<stop offset="35%" stop-color="rgba(255,255,255,0.2)" />
|
||||
<stop offset="100%" stop-color="rgba(255,255,255,0)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
|
||||
<rect id="bg-layer" width="100%" height="100%" fill="url(#bgLight)" />
|
||||
<rect id="soft-glow" width="100%" height="100%" fill="url(#glowLight)" />
|
||||
<rect width="100%" height="100%" fill="url(#glowSecondary)" />
|
||||
<rect width="100%" height="100%" fill="url(#meshLight)" />
|
||||
|
||||
|
||||
<!-- 柔和块面光影,替代明显线条 -->
|
||||
<g opacity="0.45">
|
||||
<rect x="-40" y="520" width="520" height="220" rx="180" fill="rgba(255,255,255,0.25)" />
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -23,7 +23,7 @@ type ToolbarTable = "edit" | "view" | "delete";
|
||||
export type IToolsButton = {
|
||||
name: string; // 按钮名称
|
||||
text?: string; // 按钮文本
|
||||
perm?: Array<string> | string; // 权限标识(可以是完整权限字符串如'sys:user:add'或操作权限如'add')
|
||||
perm?: Array<string> | string; // 权限标识(可以是完整权限字符串如'sys:user:create'或操作权限如'create')
|
||||
attrs?: Partial<ButtonProps> & { style?: CSSProperties }; // 按钮属性
|
||||
render?: (row: IObject) => boolean; // 条件渲染
|
||||
};
|
||||
|
||||
132
src/components/TenantSelect/index.vue
Normal file
132
src/components/TenantSelect/index.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="handleTenantSwitch">
|
||||
<div class="tenant-select">
|
||||
<el-icon class="tenant-select__icon"><OfficeBuilding /></el-icon>
|
||||
<span class="tenant-select__name">{{ currentTenantName }}</span>
|
||||
<el-icon class="tenant-select__arrow"><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="tenant in tenantList"
|
||||
:key="tenant.id"
|
||||
:command="tenant.id"
|
||||
:class="{ 'is-active': tenant.id === currentTenantId }"
|
||||
>
|
||||
<div class="tenant-item">
|
||||
<span class="tenant-item__name">{{ tenant.name }}</span>
|
||||
<el-icon v-if="tenant.id === currentTenantId" class="tenant-item__check">
|
||||
<Check />
|
||||
</el-icon>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { OfficeBuilding, ArrowDown, Check } from "@element-plus/icons-vue";
|
||||
|
||||
const tenantStore = useTenantStoreHook();
|
||||
|
||||
// 当前租户名称
|
||||
const currentTenantName = computed(() => {
|
||||
return tenantStore.currentTenant?.name || "未选择租户";
|
||||
});
|
||||
|
||||
// 当前租户ID
|
||||
const currentTenantId = computed(() => tenantStore.currentTenantId);
|
||||
|
||||
// 租户列表
|
||||
const tenantList = computed(() => tenantStore.tenantList);
|
||||
|
||||
/**
|
||||
* 切换租户
|
||||
*/
|
||||
async function handleTenantSwitch(tenantId: number) {
|
||||
if (tenantId === currentTenantId.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await tenantStore.switchTenant(tenantId);
|
||||
ElMessage.success("切换租户成功");
|
||||
// 刷新页面以重新加载菜单和权限
|
||||
window.location.reload();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || "切换租户失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化:获取租户列表
|
||||
onMounted(() => {
|
||||
tenantStore.fetchTenantList().catch((error) => {
|
||||
console.error("获取租户列表失败:", error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tenant-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&__icon {
|
||||
margin-right: 6px;
|
||||
font-size: 18px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
&__name {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
|
||||
.tenant-select__icon,
|
||||
.tenant-select__name {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tenant-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__check {
|
||||
margin-left: 8px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item.is-active) {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
</style>
|
||||
187
src/components/TenantSelectDialog/index.vue
Normal file
187
src/components/TenantSelectDialog/index.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择租户"
|
||||
width="400px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
>
|
||||
<div v-if="loading" class="tenant-dialog-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载租户列表...</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tenantList.length === 0" class="tenant-dialog-empty">
|
||||
<el-empty description="暂无可用租户" />
|
||||
</div>
|
||||
|
||||
<el-radio-group v-else v-model="selectedTenantId" class="tenant-radio-group">
|
||||
<el-radio
|
||||
v-for="tenant in tenantList"
|
||||
:key="tenant.id"
|
||||
:label="tenant.id"
|
||||
class="tenant-radio-item"
|
||||
>
|
||||
<div class="tenant-radio-content">
|
||||
<div class="tenant-radio-content__name">{{ tenant.name }}</div>
|
||||
<div v-if="tenant.code" class="tenant-radio-content__code">{{ tenant.code }}</div>
|
||||
</div>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" :loading="switching" @click="handleConfirm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Loading } from "@element-plus/icons-vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: boolean];
|
||||
confirm: [];
|
||||
}>();
|
||||
|
||||
const tenantStore = useTenantStoreHook();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit("update:modelValue", value),
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const switching = ref(false);
|
||||
const selectedTenantId = ref<number | null>(null);
|
||||
const tenantList = computed(() => tenantStore.tenantList);
|
||||
|
||||
// 监听对话框打开,加载租户列表
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal) {
|
||||
loadTenantList();
|
||||
// 默认选择当前租户或第一个租户
|
||||
selectedTenantId.value = tenantStore.currentTenantId || tenantList.value[0]?.id || null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载租户列表
|
||||
*/
|
||||
async function loadTenantList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await tenantStore.fetchTenantList();
|
||||
// 如果列表为空,自动关闭对话框
|
||||
if (tenantList.value.length === 0) {
|
||||
visible.value = false;
|
||||
ElMessage.warning("您暂无可用租户");
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || "获取租户列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认选择
|
||||
*/
|
||||
async function handleConfirm() {
|
||||
if (!selectedTenantId.value) {
|
||||
ElMessage.warning("请选择租户");
|
||||
return;
|
||||
}
|
||||
|
||||
switching.value = true;
|
||||
try {
|
||||
await tenantStore.switchTenant(selectedTenantId.value);
|
||||
ElMessage.success("切换租户成功");
|
||||
visible.value = false;
|
||||
emit("confirm");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || "切换租户失败");
|
||||
} finally {
|
||||
switching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消
|
||||
*/
|
||||
function handleCancel() {
|
||||
// 如果用户没有租户,不允许取消
|
||||
if (tenantList.value.length === 0) {
|
||||
return;
|
||||
}
|
||||
visible.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tenant-dialog-loading {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.tenant-dialog-empty {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.tenant-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tenant-radio-item {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-radio__input.is-checked) {
|
||||
.el-radio__inner {
|
||||
background-color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tenant-radio-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
&__code {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@ export const hasPerm: Directive = {
|
||||
// 校验传入的权限值是否合法
|
||||
if (!requiredPerms || (typeof requiredPerms !== "string" && !Array.isArray(requiredPerms))) {
|
||||
throw new Error(
|
||||
"需要提供权限标识!例如:v-has-perm=\"'sys:user:add'\" 或 v-has-perm=\"['sys:user:add', 'sys:user:edit']\""
|
||||
"需要提供权限标识!例如:v-has-perm=\"'sys:user:create'\" 或 v-has-perm=\"['sys:user:create', 'sys:user:update']\""
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 核心枚举定义
|
||||
export enum MenuTypeEnum {
|
||||
CATALOG = 2, // 目录
|
||||
MENU = 1, // 菜单
|
||||
BUTTON = 4, // 按钮
|
||||
EXTLINK = 3, // 外链
|
||||
CATALOG = "C", // 目录
|
||||
MENU = "M", // 菜单
|
||||
BUTTON = "B", // 按钮
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
<div class="navbar-actions__item">
|
||||
<Notification />
|
||||
</div>
|
||||
|
||||
<!-- 租户选择(如果启用多租户) -->
|
||||
<div v-if="showTenantSelect" class="navbar-actions__item">
|
||||
<TenantSelect />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 用户菜单 -->
|
||||
@@ -74,11 +79,14 @@ import Fullscreen from "@/components/Fullscreen/index.vue";
|
||||
import SizeSelect from "@/components/SizeSelect/index.vue";
|
||||
import LangSelect from "@/components/LangSelect/index.vue";
|
||||
import Notification from "@/components/Notification/index.vue";
|
||||
import TenantSelect from "@/components/TenantSelect/index.vue";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const settingStore = useSettingsStore();
|
||||
const userStore = useUserStore();
|
||||
const tenantStore = useTenantStoreHook();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -86,6 +94,11 @@ const router = useRouter();
|
||||
// 是否为桌面设备
|
||||
const isDesktop = computed(() => appStore.device === DeviceEnum.DESKTOP);
|
||||
|
||||
// 是否显示租户选择(如果用户有多个租户或已选择租户)
|
||||
const showTenantSelect = computed(() => {
|
||||
return tenantStore.tenantList.length > 0 || tenantStore.currentTenantId !== null;
|
||||
});
|
||||
|
||||
/**
|
||||
* 打开个人中心页面
|
||||
*/
|
||||
|
||||
@@ -14,4 +14,5 @@ export * from "./modules/settings-store";
|
||||
export * from "./modules/tags-view-store";
|
||||
export * from "./modules/user-store";
|
||||
export * from "./modules/dict-store";
|
||||
export * from "./modules/tenant-store";
|
||||
export { store };
|
||||
|
||||
135
src/store/modules/tenant-store.ts
Normal file
135
src/store/modules/tenant-store.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { store } from "@/store";
|
||||
import TenantAPI, { type TenantInfo } from "@/api/system/tenant-api";
|
||||
|
||||
const TENANT_ID_KEY = "current_tenant_id";
|
||||
const TENANT_INFO_KEY = "current_tenant_info";
|
||||
|
||||
/**
|
||||
* 租户 Store
|
||||
*/
|
||||
export const useTenantStore = defineStore("tenant", () => {
|
||||
// 当前租户ID
|
||||
const currentTenantId = ref<number | null>(null);
|
||||
// 当前租户信息
|
||||
const currentTenant = ref<TenantInfo | null>(null);
|
||||
// 用户可访问的租户列表
|
||||
const tenantList = ref<TenantInfo[]>([]);
|
||||
|
||||
/**
|
||||
* 初始化租户信息
|
||||
* 从 localStorage 恢复上次使用的租户
|
||||
*/
|
||||
function initTenant() {
|
||||
const savedTenantId = localStorage.getItem(TENANT_ID_KEY);
|
||||
const savedTenantInfo = localStorage.getItem(TENANT_INFO_KEY);
|
||||
|
||||
if (savedTenantId) {
|
||||
currentTenantId.value = Number(savedTenantId);
|
||||
}
|
||||
|
||||
if (savedTenantInfo) {
|
||||
try {
|
||||
currentTenant.value = JSON.parse(savedTenantInfo);
|
||||
} catch (e) {
|
||||
console.error("解析租户信息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户租户列表
|
||||
*/
|
||||
function fetchTenantList() {
|
||||
return new Promise<TenantInfo[]>((resolve, reject) => {
|
||||
TenantAPI.getTenantList()
|
||||
.then((data) => {
|
||||
tenantList.value = data || [];
|
||||
resolve(data || []);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前租户
|
||||
*
|
||||
* @param tenant 租户信息
|
||||
*/
|
||||
function setCurrentTenant(tenant: TenantInfo) {
|
||||
currentTenantId.value = tenant.id;
|
||||
currentTenant.value = tenant;
|
||||
|
||||
// 保存到 localStorage
|
||||
localStorage.setItem(TENANT_ID_KEY, String(tenant.id));
|
||||
localStorage.setItem(TENANT_INFO_KEY, JSON.stringify(tenant));
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换租户
|
||||
*
|
||||
* @param tenantId 目标租户ID
|
||||
*/
|
||||
function switchTenant(tenantId: number) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
TenantAPI.switchTenant(tenantId)
|
||||
.then((tenantInfo) => {
|
||||
// 后端返回切换后的租户信息
|
||||
if (tenantInfo) {
|
||||
setCurrentTenant(tenantInfo);
|
||||
} else {
|
||||
// 如果后端未返回,从租户列表中找到对应的租户信息
|
||||
const tenant = tenantList.value.find((t) => t.id === tenantId);
|
||||
if (tenant) {
|
||||
setCurrentTenant(tenant);
|
||||
} else {
|
||||
// 如果列表中没有,重新获取租户信息
|
||||
TenantAPI.getCurrentTenant()
|
||||
.then((info) => {
|
||||
if (info) {
|
||||
setCurrentTenant(info);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除租户信息
|
||||
*/
|
||||
function clearTenant() {
|
||||
currentTenantId.value = null;
|
||||
currentTenant.value = null;
|
||||
tenantList.value = [];
|
||||
localStorage.removeItem(TENANT_ID_KEY);
|
||||
localStorage.removeItem(TENANT_INFO_KEY);
|
||||
}
|
||||
|
||||
// 初始化
|
||||
initTenant();
|
||||
|
||||
return {
|
||||
currentTenantId,
|
||||
currentTenant,
|
||||
tenantList,
|
||||
fetchTenantList,
|
||||
setCurrentTenant,
|
||||
switchTenant,
|
||||
clearTenant,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 在组件外部使用 TenantStore 的钩子函数
|
||||
*/
|
||||
export function useTenantStoreHook() {
|
||||
return useTenantStore(store);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { ApiCodeEnum } from "@/enums/api/code-enum";
|
||||
import { AuthStorage, redirectToLogin } from "@/utils/auth";
|
||||
import { useTokenRefresh } from "@/composables/auth/useTokenRefresh";
|
||||
import { authConfig } from "@/settings";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
|
||||
// 初始化token刷新组合式函数
|
||||
const { refreshTokenAndRetry } = useTokenRefresh();
|
||||
@@ -19,7 +20,7 @@ const httpRequest = axios.create({
|
||||
});
|
||||
|
||||
/**
|
||||
* 请求拦截器 - 添加 Authorization 头
|
||||
* 请求拦截器 - 添加 Authorization 头和租户ID
|
||||
*/
|
||||
httpRequest.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
@@ -32,6 +33,18 @@ httpRequest.interceptors.request.use(
|
||||
delete config.headers.Authorization;
|
||||
}
|
||||
|
||||
// 添加租户ID到请求头(如果存在)
|
||||
try {
|
||||
const tenantStore = useTenantStoreHook();
|
||||
const tenantId = tenantStore.currentTenantId;
|
||||
if (tenantId) {
|
||||
config.headers["tenant-id"] = String(tenantId);
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果租户 store 未初始化,忽略错误
|
||||
console.debug("Tenant store not available:", error);
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="provider" label="AI提供商">
|
||||
<el-form-item prop="aiProvider" label="AI提供商">
|
||||
<el-select
|
||||
v-model="queryParams.provider"
|
||||
v-model="queryParams.aiProvider"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
@@ -27,9 +27,9 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="model" label="AI模型">
|
||||
<el-form-item prop="aiModel" label="AI模型">
|
||||
<el-input
|
||||
v-model="queryParams.model"
|
||||
v-model="queryParams.aiModel"
|
||||
placeholder="如 qwen-plus"
|
||||
clearable
|
||||
style="width: 160px"
|
||||
@@ -37,15 +37,15 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="parseSuccess" label="解析状态">
|
||||
<el-form-item prop="parseStatus" label="解析状态">
|
||||
<el-select
|
||||
v-model="queryParams.parseSuccess"
|
||||
v-model="queryParams.parseStatus"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
>
|
||||
<el-option label="成功" :value="true" />
|
||||
<el-option label="失败" :value="false" />
|
||||
<el-option label="成功" :value="1" />
|
||||
<el-option label="失败" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -56,21 +56,9 @@
|
||||
clearable
|
||||
style="width: 140px"
|
||||
>
|
||||
<el-option label="待执行" value="pending" />
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="isDangerous" label="风险操作">
|
||||
<el-select
|
||||
v-model="queryParams.isDangerous"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
>
|
||||
<el-option label="是" :value="true" />
|
||||
<el-option label="否" :value="false" />
|
||||
<el-option label="待执行" :value="0" />
|
||||
<el-option label="成功" :value="1" />
|
||||
<el-option label="失败" :value="-1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -117,29 +105,27 @@
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="AI提供商" prop="provider" width="120" />
|
||||
<el-table-column label="AI模型" prop="model" width="160" show-overflow-tooltip />
|
||||
<el-table-column label="AI提供商" prop="aiProvider" width="120" />
|
||||
<el-table-column label="AI模型" prop="aiModel" width="160" show-overflow-tooltip />
|
||||
<el-table-column label="解析状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.parseSuccess ? 'success' : 'danger'" size="small">
|
||||
{{ row.parseSuccess ? "成功" : "失败" }}
|
||||
<el-tag :type="row.parseStatus === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ row.parseStatus === 1 ? "成功" : "失败" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.executeStatus" :type="statusTagType[row.executeStatus]" size="small">
|
||||
{{ statusText[row.executeStatus] }}
|
||||
<el-tag
|
||||
v-if="row.executeStatus !== null && row.executeStatus !== undefined"
|
||||
:type="getExecuteStatusTagType(row.executeStatus)"
|
||||
size="small"
|
||||
>
|
||||
{{ getExecuteStatusText(row.executeStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="风险" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.isDangerous" type="warning" size="small">风险</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="置信度" prop="confidence" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.confidence !== undefined && row.confidence !== null">
|
||||
@@ -148,8 +134,7 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="解析耗时(ms)" prop="parseTime" width="120" align="center" />
|
||||
<el-table-column label="执行耗时(ms)" prop="executionTime" width="120" align="center" />
|
||||
<el-table-column label="解析耗时(ms)" prop="parseDurationMs" width="120" align="center" />
|
||||
<el-table-column label="IP地址" prop="ipAddress" width="140" />
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
@@ -180,15 +165,15 @@
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="AI提供商">
|
||||
{{ currentRow.provider || "-" }}
|
||||
{{ currentRow.aiProvider || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="AI模型">
|
||||
{{ currentRow.model || "-" }}
|
||||
{{ currentRow.aiModel || "-" }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="解析状态">
|
||||
<el-tag :type="currentRow.parseSuccess ? 'success' : 'danger'" size="small">
|
||||
{{ currentRow.parseSuccess ? "成功" : "失败" }}
|
||||
<el-tag :type="currentRow.parseStatus === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ currentRow.parseStatus === 1 ? "成功" : "失败" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="置信度">
|
||||
@@ -199,11 +184,10 @@
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="解析耗时">
|
||||
{{ formatNumber(currentRow.parseTime) }} ms
|
||||
{{ formatNumber(currentRow.parseDurationMs) }} ms
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Token统计">
|
||||
输入 {{ currentRow.inputTokens || 0 }} / 输出 {{ currentRow.outputTokens || 0 }} / 总计
|
||||
{{ currentRow.totalTokens || 0 }}
|
||||
输入 {{ currentRow.inputTokens || 0 }} / 输出 {{ currentRow.outputTokens || 0 }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="原始命令" :span="2">
|
||||
@@ -232,22 +216,15 @@
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行状态">
|
||||
<el-tag
|
||||
v-if="currentRow.executeStatus"
|
||||
:type="statusTagType[currentRow.executeStatus]"
|
||||
v-if="currentRow.executeStatus !== null && currentRow.executeStatus !== undefined"
|
||||
:type="getExecuteStatusTagType(currentRow.executeStatus)"
|
||||
size="small"
|
||||
>
|
||||
{{ statusText[currentRow.executeStatus] }}
|
||||
{{ getExecuteStatusText(currentRow.executeStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="执行耗时">
|
||||
{{ formatNumber(currentRow.executionTime) }} ms
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="影响行数">
|
||||
{{ formatNumber(currentRow.affectedRows) }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="currentRow.functionArguments" label="执行参数" :span="2">
|
||||
<el-input
|
||||
:model-value="formatJson(currentRow.functionArguments)"
|
||||
@@ -257,39 +234,13 @@
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="currentRow.executeResult" label="执行结果" :span="2">
|
||||
<el-input
|
||||
:model-value="formatJson(currentRow.executeResult)"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
readonly
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="currentRow.executeErrorMessage" label="执行错误" :span="2">
|
||||
<el-alert :title="currentRow.executeErrorMessage" type="error" :closable="false" />
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="风险操作">
|
||||
<el-tag v-if="currentRow.isDangerous" type="warning" size="small">风险操作</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="是否确认">
|
||||
<span v-if="currentRow.requiresConfirmation">
|
||||
{{ currentRow.userConfirmed ? "已确认" : "待确认" }}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="IP地址">
|
||||
{{ currentRow.ipAddress || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="页面路由">
|
||||
{{ currentRow.currentRoute || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="User-Agent" :span="2">
|
||||
{{ currentRow.userAgent || "-" }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ currentRow.createTime }}
|
||||
@@ -297,9 +248,6 @@
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ currentRow.updateTime || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ currentRow.remark || "-" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template #footer>
|
||||
@@ -327,11 +275,10 @@ const queryParams = reactive<AiCommandRecordPageQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
keywords: "",
|
||||
provider: "",
|
||||
model: "",
|
||||
parseSuccess: undefined,
|
||||
executeStatus: "",
|
||||
isDangerous: undefined,
|
||||
aiProvider: "",
|
||||
aiModel: "",
|
||||
parseStatus: undefined,
|
||||
executeStatus: undefined,
|
||||
createTime: ["", ""],
|
||||
});
|
||||
|
||||
@@ -340,17 +287,31 @@ const pageData = ref<AiCommandRecordVO[]>([]);
|
||||
const detailDialogVisible = ref(false);
|
||||
const currentRow = ref<AiCommandRecordVO>();
|
||||
|
||||
const statusText: Record<string, string> = {
|
||||
pending: "待执行",
|
||||
success: "成功",
|
||||
failed: "失败",
|
||||
};
|
||||
function getExecuteStatusText(status: number): string {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return "待执行";
|
||||
case 1:
|
||||
return "成功";
|
||||
case -1:
|
||||
return "失败";
|
||||
default:
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
const statusTagType: Record<string, "info" | "success" | "danger"> = {
|
||||
pending: "info",
|
||||
success: "success",
|
||||
failed: "danger",
|
||||
};
|
||||
function getExecuteStatusTagType(status: number): "info" | "success" | "danger" {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return "info";
|
||||
case 1:
|
||||
return "success";
|
||||
case -1:
|
||||
return "danger";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
}
|
||||
|
||||
function fetchData() {
|
||||
loading.value = true;
|
||||
|
||||
@@ -86,6 +86,9 @@
|
||||
</el-link>
|
||||
</div>
|
||||
|
||||
<!-- 租户选择对话框 -->
|
||||
<TenantSelectDialog v-model="tenantDialogVisible" @confirm="handleTenantSelected" />
|
||||
|
||||
<!-- 第三方登录 -->
|
||||
<div class="third-party-login">
|
||||
<div class="divider-container">
|
||||
@@ -115,8 +118,10 @@ import type { FormInstance } from "element-plus";
|
||||
import AuthAPI, { type LoginFormData } from "@/api/auth-api";
|
||||
import router from "@/router";
|
||||
import { useUserStore } from "@/store";
|
||||
import { useTenantStoreHook } from "@/store/modules/tenant-store";
|
||||
import CommonWrapper from "@/components/CommonWrapper/index.vue";
|
||||
import { AuthStorage } from "@/utils/auth";
|
||||
import TenantSelectDialog from "@/components/TenantSelectDialog/index.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
@@ -132,6 +137,9 @@ const isCapsLock = ref(false);
|
||||
const captchaBase64 = ref();
|
||||
// 记住我
|
||||
const rememberMe = AuthStorage.getRememberMe();
|
||||
// 租户选择对话框
|
||||
const tenantDialogVisible = ref(false);
|
||||
const tenantStore = useTenantStoreHook();
|
||||
|
||||
const loginFormData = ref<LoginFormData>({
|
||||
username: "admin",
|
||||
@@ -198,11 +206,31 @@ async function handleLoginSubmit() {
|
||||
// 2. 执行登录
|
||||
await userStore.login(loginFormData.value);
|
||||
|
||||
const redirectPath = (route.query.redirect as string) || "/";
|
||||
// 3. 登录成功后,检查是否需要选择租户
|
||||
try {
|
||||
await tenantStore.fetchTenantList();
|
||||
const tenantList = tenantStore.tenantList;
|
||||
|
||||
// 如果用户有多个租户,且没有当前租户,显示租户选择对话框
|
||||
if (tenantList.length > 1 && !tenantStore.currentTenantId) {
|
||||
tenantDialogVisible.value = true;
|
||||
return; // 等待用户选择租户
|
||||
}
|
||||
|
||||
// 如果只有一个租户,自动设置
|
||||
if (tenantList.length === 1 && !tenantStore.currentTenantId) {
|
||||
await tenantStore.switchTenant(tenantList[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果获取租户列表失败,不影响登录流程(可能是单租户模式)
|
||||
console.warn("获取租户列表失败(可能是单租户模式):", error);
|
||||
}
|
||||
|
||||
// 4. 跳转到目标页面
|
||||
const redirectPath = (route.query.redirect as string) || "/";
|
||||
await router.push(decodeURIComponent(redirectPath));
|
||||
} catch (error) {
|
||||
// 4. 统一错误处理
|
||||
// 统一错误处理
|
||||
getCaptcha(); // 刷新验证码
|
||||
console.error("登录失败:", error);
|
||||
} finally {
|
||||
@@ -210,6 +238,14 @@ async function handleLoginSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户选择确认后的处理
|
||||
*/
|
||||
async function handleTenantSelected() {
|
||||
const redirectPath = (route.query.redirect as string) || "/";
|
||||
await router.push(decodeURIComponent(redirectPath));
|
||||
}
|
||||
|
||||
// 检查输入大小写
|
||||
function checkCapsLock(event: KeyboardEvent) {
|
||||
// 防止浏览器密码自动填充时报错
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="table-section__toolbar">
|
||||
<div class="table-section__toolbar--actions">
|
||||
<el-button
|
||||
v-hasPerm="['sys:config:add']"
|
||||
v-hasPerm="['sys:config:create']"
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleOpenDialog()"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<div class="table-section__toolbar">
|
||||
<div class="table-section__toolbar--actions">
|
||||
<el-button
|
||||
v-hasPerm="['sys:dept:add']"
|
||||
v-hasPerm="['sys:dept:create']"
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleOpenDialog()"
|
||||
@@ -74,7 +74,7 @@
|
||||
<el-table-column label="操作" fixed="right" align="left" width="200">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPerm="['sys:dept:add']"
|
||||
v-hasPerm="['sys:dept:create']"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@@ -84,7 +84,7 @@
|
||||
新增
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['sys:dept:edit']"
|
||||
v-hasPerm="['sys:dept:update']"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="table-section__toolbar">
|
||||
<div class="table-section__toolbar--actions">
|
||||
<el-button
|
||||
v-hasPerm="['sys:menu:add']"
|
||||
v-hasPerm="['sys:menu:create']"
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleOpenDialog('0')"
|
||||
@@ -64,7 +64,6 @@
|
||||
<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="路由名称" align="left" width="150" prop="routeName" />
|
||||
@@ -82,7 +81,7 @@
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-if="scope.row.type == MenuTypeEnum.CATALOG || scope.row.type == MenuTypeEnum.MENU"
|
||||
v-hasPerm="['sys:menu:add']"
|
||||
v-hasPerm="['sys:menu:create']"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@@ -93,7 +92,7 @@
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-hasPerm="['sys:menu:edit']"
|
||||
v-hasPerm="['sys:menu:update']"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@@ -144,15 +143,10 @@
|
||||
<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.routePath" placeholder="请输入外链完整路径" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="routeName">
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.MENU && !isExternalLink" prop="routeName">
|
||||
<template #label>
|
||||
<div class="flex-y-center">
|
||||
路由名称
|
||||
@@ -192,10 +186,10 @@
|
||||
v-model="formData.routePath"
|
||||
placeholder="system"
|
||||
/>
|
||||
<el-input v-else v-model="formData.routePath" placeholder="user" />
|
||||
<el-input v-else v-model="formData.routePath" placeholder="user 或 https://example.com" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="component">
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.MENU && !isExternalLink" prop="component">
|
||||
<template #label>
|
||||
<div class="flex-y-center">
|
||||
组件路径
|
||||
@@ -216,7 +210,7 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.MENU">
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.MENU && !isExternalLink">
|
||||
<template #label>
|
||||
<div class="flex-y-center">
|
||||
路由参数
|
||||
@@ -298,7 +292,10 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type === MenuTypeEnum.MENU" label="缓存页面">
|
||||
<el-form-item
|
||||
v-if="formData.type === MenuTypeEnum.MENU && !isExternalLink"
|
||||
label="缓存页面"
|
||||
>
|
||||
<el-radio-group v-model="formData.keepAlive">
|
||||
<el-radio :value="1">开启</el-radio>
|
||||
<el-radio :value="0">关闭</el-radio>
|
||||
@@ -316,7 +313,7 @@
|
||||
|
||||
<!-- 权限标识 -->
|
||||
<el-form-item v-if="formData.type == MenuTypeEnum.BUTTON" label="权限标识" prop="perm">
|
||||
<el-input v-model="formData.perm" placeholder="sys:user:add" />
|
||||
<el-input v-model="formData.perm" placeholder="sys:user:create" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="formData.type !== MenuTypeEnum.BUTTON" label="图标" prop="icon">
|
||||
@@ -382,14 +379,34 @@ const initialMenuFormData = ref<MenuForm>({
|
||||
});
|
||||
// 菜单表单数据
|
||||
const formData = ref({ ...initialMenuFormData.value });
|
||||
const isExternalLink = computed(
|
||||
() =>
|
||||
formData.value.type === MenuTypeEnum.MENU &&
|
||||
!!formData.value.routePath &&
|
||||
/^https?:\/\//.test(formData.value.routePath)
|
||||
);
|
||||
const validateRouteName = (_: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (formData.value.type === MenuTypeEnum.MENU && !isExternalLink.value && !value) {
|
||||
callback(new Error("请输入路由名称"));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
};
|
||||
const validateComponent = (_: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (formData.value.type === MenuTypeEnum.MENU && !isExternalLink.value && !value) {
|
||||
callback(new Error("请输入组件路径"));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
};
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
parentId: [{ required: true, message: "请选择父级菜单", trigger: "blur" }],
|
||||
name: [{ required: true, message: "请输入菜单名称", trigger: "blur" }],
|
||||
type: [{ required: true, message: "请选择菜单类型", trigger: "blur" }],
|
||||
routeName: [{ required: true, message: "请输入路由名称", trigger: "blur" }],
|
||||
routeName: [{ validator: validateRouteName, trigger: "blur" }],
|
||||
routePath: [{ required: true, message: "请输入路由路径", trigger: "blur" }],
|
||||
component: [{ required: true, message: "请输入组件路径", trigger: "blur" }],
|
||||
component: [{ validator: validateComponent, trigger: "blur" }],
|
||||
visible: [{ required: true, message: "请选择显示状态", trigger: "change" }],
|
||||
});
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="table-section__toolbar">
|
||||
<div class="table-section__toolbar--actions">
|
||||
<el-button
|
||||
v-hasPerm="['sys:notice:add']"
|
||||
v-hasPerm="['sys:notice:create']"
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleOpenDialog()"
|
||||
@@ -134,7 +134,7 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.publishStatus != 1"
|
||||
v-hasPerm="['sys:notice:edit']"
|
||||
v-hasPerm="['sys:notice:update']"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<div class="table-section__toolbar">
|
||||
<div class="table-section__toolbar--actions">
|
||||
<el-button
|
||||
v-hasPerm="['sys:user:add']"
|
||||
v-hasPerm="['sys:user:create']"
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleOpenDialog()"
|
||||
@@ -130,7 +130,7 @@
|
||||
重置密码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="'sys:user:edit'"
|
||||
v-hasPerm="'sys:user:update'"
|
||||
type="primary"
|
||||
icon="edit"
|
||||
link
|
||||
|
||||
Reference in New Issue
Block a user