feat
This commit is contained in:
22
.env.development
Normal file
22
.env.development
Normal file
@@ -0,0 +1,22 @@
|
||||
# 开发配置文件
|
||||
|
||||
# 应用工作空间名称
|
||||
VITE_APP_WORKSPACE=ops
|
||||
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=标准管理系统
|
||||
VITE_APP_DESCRIPTION="default standard template"
|
||||
|
||||
# API 基础URL
|
||||
VITE_API_BASE_URL=https://ops-api.apinb.com
|
||||
|
||||
|
||||
|
||||
# 应用版本
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
# 是否启用调试模式
|
||||
VITE_DEBUG=true
|
||||
|
||||
# 主题配置
|
||||
VITE_DEFAULT_LANGUAGE=cn
|
||||
19
.env.production
Normal file
19
.env.production
Normal file
@@ -0,0 +1,19 @@
|
||||
# 生产环境变量
|
||||
|
||||
# 应用工作空间名称
|
||||
VITE_APP_WORKSPACE=ops
|
||||
|
||||
VITE_APP_TITLE=标准管理系统
|
||||
VITE_APP_DESCRIPTION="default standard template"
|
||||
|
||||
# API 基础URL
|
||||
VITE_API_BASE_URL=https://ops-api.apinb.com
|
||||
|
||||
# 应用版本
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
# 是否启用调试模式
|
||||
VITE_DEBUG=false
|
||||
|
||||
# 主题配置
|
||||
VITE_DEFAULT_LANGUAGE=cn
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vue Admin Arco - 开箱即用的中台前端/设计解决方案</title>
|
||||
<title>智能运维管理系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.57.0",
|
||||
"@tabler/icons-vue": "^3.40.0",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"axios": "^1.13.6",
|
||||
"dayjs": "^1.11.19",
|
||||
@@ -28,6 +29,7 @@
|
||||
"pinia": "^3.0.4",
|
||||
"query-string": "^9.3.1",
|
||||
"sortablejs": "^1.15.7",
|
||||
"uuid": "^13.0.0",
|
||||
"vue": "^3.5.29",
|
||||
"vue-echarts": "^8.0.1",
|
||||
"vue-i18n": "^11.2.8",
|
||||
|
||||
35
src/api/module/factor.ts
Normal file
35
src/api/module/factor.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
/** 批次列表 */
|
||||
export const fetchLoanBatch = (data: { page: number, size: number }) => request.post("/factor/v1/fetch/loan_batch", data);
|
||||
|
||||
/** 上报列表 */
|
||||
export const fetchTransferWatch = (data: { page: number, size: number }) => request.post("/factor/v1/fetch/transfer_watch", data);
|
||||
|
||||
/** 回盘列表 */
|
||||
export const fetchTransferDown = (data: { page: number, size: number }) => request.post("/factor/v1/fetch/transfer_down", data);
|
||||
|
||||
/** 放款数据列表 */
|
||||
export const fetchDataFile = (data: { page: number, size: number }) => request.post("/factor/v1/fetch/data_file", data);
|
||||
|
||||
/** 回盘数据列表 */
|
||||
export const fetchDataReply = (data: { page: number, size: number }) => request.post("/factor/v1/fetch/data_reply", data);
|
||||
|
||||
/** 贷款支付数据列表 */
|
||||
export const fetchLoanPayment = (data: { page: number, size: number, status?: number }) => request.post("/factor/v1/fetch/loan_payment", data);
|
||||
|
||||
/** 获取日报数据 */
|
||||
export const fetchOverviewReports = (data: { page: number, size: number }) => request.post("/factor/v1/reports/overview", data);
|
||||
|
||||
/** 获取日报数据 */
|
||||
export const fetchDailyReports = (data: { page: number, size: number }) => request.post("/factor/v1/reports/days", data);
|
||||
|
||||
/** 获取周报数据 */
|
||||
export const fetchWeeklyReports = (data: { page: number, size: number }) => request.post("/factor/v1/reports/weeks", data);
|
||||
|
||||
/** 获取月报数据 */
|
||||
export const fetchMonthlyReports = (data: { page: number, size: number }) => request.post("/factor/v1/reports/months", data);
|
||||
|
||||
|
||||
/** 获取批次详情 */
|
||||
export const getLoanBatchDetail = (data: { batch_id: number }) => request.post(`/factor/v1/get/batch`,data);
|
||||
39
src/api/module/pmn.ts
Normal file
39
src/api/module/pmn.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
export interface MenuItem {
|
||||
id?: number;
|
||||
identity?: string;
|
||||
code?: string;
|
||||
description?: string;
|
||||
menu_icon?: string;
|
||||
menu_path?: string;
|
||||
parent_id?: number | null;
|
||||
title?: string;
|
||||
title_en?: string;
|
||||
type?: number;
|
||||
sort_key?: number;
|
||||
order?: number; // 排序字段(用于本地排序)
|
||||
app_id?: number;
|
||||
created_at?: string;
|
||||
children?: MenuItem[];
|
||||
is_web_page?: boolean; // 是否为网页
|
||||
web_url?: string; // 嵌入的网页URL
|
||||
}
|
||||
|
||||
/** 获取菜单 */
|
||||
export const fetchMenu = (data: { page: number, size: number }) => request.post("/rbac2/v1/pmn/fetch", data, { needWorkspace: true });
|
||||
/** 创建菜单 */
|
||||
export const createMenu = (data: MenuItem) => request.post("/rbac2/v1/pmn/create", data, { needWorkspace: true });
|
||||
/** 修改菜单 */
|
||||
export const modifyMenu = (data: MenuItem) => request.post("/rbac2/v1/pmn/modify", data, { needWorkspace: true });
|
||||
/** 删除菜单 */
|
||||
export const deleteMenu = (data: { id: MenuItem['id'] }) => request.post("/rbac2/v1/pmn/del", data, { needWorkspace: true });
|
||||
/** 更新菜单排序 */
|
||||
export const updateMenuOrder = (data: { pmn_id: number, sort_key: number }[]) =>
|
||||
request.post("/rbac2/v1/pmn/sort", data);
|
||||
|
||||
/** 用户-给指定用户设置权限 */
|
||||
export const userPmn = (data: { code: string; list: { id: number, pmn_id: number }[] }) => request.post("/rbac2/v1/pmn/user_pmn", data, { needWorkspace: true });
|
||||
|
||||
/** 用户-给指定用户设置权限 */
|
||||
export const userSetPmn = (data: { list: { id: number, pmn_id: number }[] }) => request.post("/rbac2/v1/pmn/user/set_pmn", data, { needWorkspace: true });
|
||||
35
src/api/module/report.ts
Normal file
35
src/api/module/report.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { request } from "@/api/request";
|
||||
|
||||
|
||||
/** 获取报表 */
|
||||
export const fetchReports = (data: { page: number, size: number, keyword?: string }) => request.get("/api/v1/report/basics", { params: data });
|
||||
|
||||
/** 创建报表任务 */
|
||||
export const fetchReportNewTask = (keyName:string,data: any) => request.post("/api/v1/report/task/new/"+keyName, data);
|
||||
|
||||
/** 获取报表基础信息详情 */
|
||||
export const fetchReportBasicInfo = (id: string) => request.get(`/api/v1/report/basics/${id}`);
|
||||
|
||||
/** 获取指定报表的调度列表 */
|
||||
export const fetchReportSchedules = (report_basic_id: string) => request.get(`/api/v1/report/schedules/${report_basic_id}`);
|
||||
|
||||
/** 批量创建报表调度 */
|
||||
export const fetchReportSchedulesBatch = (data: any) => request.post("/api/v1/report/schedules/batch", data);
|
||||
|
||||
/** 获取指定报表的历史记录列表 */
|
||||
export const fetchReportHistories = (report_basic_id: string) => request.get(`/api/v1/report/histories/${report_basic_id}?status=2`);
|
||||
|
||||
/** 获取报表历史记录列表 */
|
||||
export const fetchReportHistoriesList = (data: { page: number, size: number, keyword?: string }) => request.get("/api/v1/report/histories", { params: data });
|
||||
|
||||
/** 获取邮件推送记录列表 */
|
||||
export enum PushStatus {
|
||||
/** 发送成功 */
|
||||
Success = 1,
|
||||
/** 发送失败 */
|
||||
Failed = 2
|
||||
}
|
||||
export const fetchReportPushesList = (data: { page: number, size: number, keyword?: string, status?: PushStatus }) => request.get("/api/v1/report/pushes", { params: data });
|
||||
|
||||
/** 重新发送邮件 */
|
||||
export const fetchReportPushesResend = (id: number) => request.post(`/api/v1/report/pushes/${id}`);
|
||||
41
src/api/module/user.ts
Normal file
41
src/api/module/user.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { request } from "@/api/request";
|
||||
import { LoginRequest, UserItem } from "../types";
|
||||
|
||||
/** 登录 */
|
||||
export const login = (data: LoginRequest) => request.post("/rbac2/v1/login", data);
|
||||
/** 登出 */
|
||||
export const logout = () => request.post("/rbac2/v1/logout");
|
||||
/** 获取菜单列表 */
|
||||
export const getMenuList = () => request.post("/rbac2/v1/user/menu");
|
||||
/** 创建用户 */
|
||||
export const createUser = (data: UserItem) => request.post("/rbac2/v1/user/create", data, { needWorkspace: true });
|
||||
/** 获取用户详情 */
|
||||
export const getUserDetail = (data: UserItem) => request.post("/rbac2/v1/user/detail", data);
|
||||
/** 获取用户列表 */
|
||||
export const fetchUserList = (data: { page: number, size: number, keyword?: string }) => request.post("/rbac2/v1/user/fetch", data);
|
||||
/** 修改用户 */
|
||||
export const modifyUser = (data: UserItem) => request.post("/rbac2/v1/user/modify", data);
|
||||
/** 删除用户 */
|
||||
export const deleteUser = (data: { id: UserItem['id'] }) => request.post("/rbac2/v1/user/del", data);
|
||||
/** 获取所有权限【树形】 */
|
||||
export const getUserPmnTree = (data: { id: UserItem['id'] }) => request.post("/rbac2/v1/user/pmn_tree", data);
|
||||
|
||||
/** 用户-获取所有权限【平面】 */
|
||||
export const userPmn = (data: { id: UserItem['id'], workspace: string }) => request.post("/rbac2/v1/user/pmn", data, { needWorkspace: true });
|
||||
/** 用户-给指定用户设置权限 */
|
||||
export const userSetPmn = (data: { list: { id: number, pmn_id: number }[] }) => request.post("/rbac2/v1/user/set_pmn", data, { needWorkspace: true });
|
||||
|
||||
/** 用户-给指定用户编辑权限 */
|
||||
export const userModifyPmn = (data: { id: UserItem['id'], list: number[] }) => request.post("/rbac2/v1/user/modify_pmn", data, { needWorkspace: true });
|
||||
|
||||
/** 重置密码(短信) */
|
||||
export const resetUserPassword = (data: {
|
||||
/** 账号 */
|
||||
account: string;
|
||||
/** 验证码 */
|
||||
code: string;
|
||||
/** 密码 */
|
||||
password: string;
|
||||
/** 手机号 */
|
||||
phone: string;
|
||||
}) => request.post("/rbac2/v1/reset", data);
|
||||
91
src/api/request.ts
Normal file
91
src/api/request.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import axios, {
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
InternalAxiosRequestConfig
|
||||
} from "axios";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage";
|
||||
|
||||
console.log('import.meta.env.VITE_API_BASE_UR:', import.meta.env.VITE_API_BASE_URL)
|
||||
// 1. 创建axios实例
|
||||
const instance: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Workspace": import.meta.env.VITE_APP_WORKSPACE,
|
||||
"Request-Id": uuidv4(),
|
||||
},
|
||||
});
|
||||
|
||||
// 2. 请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// 添加认证token(示例)
|
||||
const token = SafeStorage.get(AppStorageKey.TOKEN);
|
||||
if (token) {
|
||||
config.headers.Authorization = token as string;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// 3. 响应拦截器
|
||||
instance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 统一处理响应数据格式[2](@ref)
|
||||
if (response.data.status === 401) {
|
||||
// token过期处理
|
||||
SafeStorage.clearAppStorage();
|
||||
window.location.href = "/auth/login";
|
||||
}
|
||||
return response.data; // 直接返回核心数据[1](@ref)
|
||||
},
|
||||
(error) => {
|
||||
if (error?.response?.data?.error === 'Token has expired') {
|
||||
// token过期处理
|
||||
SafeStorage.clearAppStorage();
|
||||
window.location.href = "/auth/login";
|
||||
}
|
||||
// 统一错误处理
|
||||
console.error("API Error:", error.message);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 4. 封装核心请求方法
|
||||
interface RequestConfig extends AxiosRequestConfig {
|
||||
data?: unknown;
|
||||
needWorkspace?: boolean;
|
||||
}
|
||||
|
||||
export const request = {
|
||||
get<T = any>(url: string, config?: RequestConfig): Promise<T> {
|
||||
return instance.get(url, config);
|
||||
},
|
||||
post<T = any>(url: string, data = {}, config?: RequestConfig): Promise<T> {
|
||||
let params: any
|
||||
if (config?.needWorkspace) {
|
||||
params = { workspace: import.meta.env.VITE_APP_WORKSPACE, ...data };
|
||||
} else {
|
||||
params = data;
|
||||
}
|
||||
return instance.post(url, params, config);
|
||||
},
|
||||
put<T = any>(url: string, data = {}, config?: RequestConfig): Promise<T> {
|
||||
let params: any
|
||||
if (config?.needWorkspace) {
|
||||
params = { workspace: import.meta.env.VITE_APP_WORKSPACE, ...data };
|
||||
} else {
|
||||
params = data;
|
||||
}
|
||||
return instance.put(url, params, config);
|
||||
},
|
||||
delete<T = any>(url: string, config?: RequestConfig): Promise<T> {
|
||||
return instance.delete(url, config);
|
||||
}
|
||||
};
|
||||
|
||||
export default instance;
|
||||
39
src/api/types.ts
Normal file
39
src/api/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/** 登录请求参数 */
|
||||
export interface LoginRequest {
|
||||
/** 账号 */
|
||||
account: string
|
||||
/** 密码 */
|
||||
password: string
|
||||
}
|
||||
|
||||
/** 登录表单数据 */
|
||||
export interface LoginData {
|
||||
account: string
|
||||
password: string
|
||||
}
|
||||
|
||||
/** 用户信息 */
|
||||
export interface UserItem {
|
||||
id?: number
|
||||
username?: string
|
||||
nickname?: string
|
||||
email?: string
|
||||
phone?: string
|
||||
avatar?: string
|
||||
status?: number
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
/** 登录响应 */
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
user?: UserItem
|
||||
}
|
||||
|
||||
/** 通用响应结构 */
|
||||
export interface ApiResponse<T = any> {
|
||||
status: number
|
||||
data: T
|
||||
message?: string
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { UserState } from '@/store/modules/user/types'
|
||||
import axios from 'axios'
|
||||
import type { RouteRecordNormalized } from 'vue-router'
|
||||
|
||||
export interface LoginData {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface LoginRes {
|
||||
token: string
|
||||
}
|
||||
export function login(data: LoginData) {
|
||||
return axios.post<LoginRes>('/api/user/login', data)
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return axios.post<LoginRes>('/api/user/logout')
|
||||
}
|
||||
|
||||
export function getUserInfo() {
|
||||
return axios.post<UserState>('/api/user/info')
|
||||
}
|
||||
|
||||
export function getMenuList() {
|
||||
return axios.post<RouteRecordNormalized[]>('/api/user/menu')
|
||||
}
|
||||
299
src/components/data-table/index.vue
Normal file
299
src/components/data-table/index.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div class="data-table">
|
||||
<!-- 工具栏 -->
|
||||
<a-row v-if="showToolbar" style="margin-bottom: 16px">
|
||||
<a-col :span="12">
|
||||
<a-space>
|
||||
<slot name="toolbar-left" />
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :span="12" style="display: flex; align-items: center; justify-content: end">
|
||||
<slot name="toolbar-right" />
|
||||
<a-button v-if="showDownload" @click="handleDownload">
|
||||
<template #icon>
|
||||
<icon-download />
|
||||
</template>
|
||||
{{ downloadButtonText }}
|
||||
</a-button>
|
||||
<a-tooltip v-if="showRefresh" :content="refreshTooltipText">
|
||||
<div class="action-icon" @click="handleRefresh"><icon-refresh size="18" /></div>
|
||||
</a-tooltip>
|
||||
<a-dropdown v-if="showDensity" @select="handleSelectDensity">
|
||||
<a-tooltip :content="densityTooltipText">
|
||||
<div class="action-icon"><icon-line-height size="18" /></div>
|
||||
</a-tooltip>
|
||||
<template #content>
|
||||
<a-doption v-for="item in densityList" :key="item.value" :value="item.value" :class="{ active: item.value === size }">
|
||||
<span>{{ item.name }}</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-tooltip v-if="showColumnSetting" :content="columnSettingTooltipText">
|
||||
<a-popover trigger="click" position="bl" @popup-visible-change="popupVisibleChange">
|
||||
<div class="action-icon"><icon-settings size="18" /></div>
|
||||
<template #content>
|
||||
<div ref="columnSettingRef" class="column-setting-container">
|
||||
<div v-for="(item, idx) in showColumns" :key="item.dataIndex" class="setting">
|
||||
<div style="margin-right: 4px; cursor: move">
|
||||
<icon-drag-arrow />
|
||||
</div>
|
||||
<div>
|
||||
<a-checkbox v-model="item.checked" @change="handleChange($event, item, idx)"></a-checkbox>
|
||||
</div>
|
||||
<div class="title">
|
||||
{{ item.title === '#' ? '序列号' : item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 表格 -->
|
||||
<a-table
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:columns="cloneColumns"
|
||||
:data="data"
|
||||
:bordered="bordered"
|
||||
:size="size"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
<!-- 动态插槽:根据 columns 的 slotName 动态渲染 -->
|
||||
<template v-for="col in slotColumns" :key="col.dataIndex" #[String(col.slotName)]="slotProps">
|
||||
<slot :name="col.slotName" v-bind="slotProps" />
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch, nextTick, onUnmounted, PropType } from 'vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
type SizeProps = 'mini' | 'small' | 'medium' | 'large'
|
||||
type Column = TableColumnData & { checked?: boolean }
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => [],
|
||||
},
|
||||
columns: {
|
||||
type: Array as PropType<TableColumnData[]>,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
pagination: {
|
||||
type: Object as PropType<{
|
||||
current: number
|
||||
pageSize: number
|
||||
total?: number
|
||||
}>,
|
||||
default: () => ({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
}),
|
||||
},
|
||||
bordered: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showToolbar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showDownload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showRefresh: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showDensity: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showColumnSetting: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
downloadButtonText: {
|
||||
type: String,
|
||||
default: '下载',
|
||||
},
|
||||
refreshTooltipText: {
|
||||
type: String,
|
||||
default: '刷新',
|
||||
},
|
||||
densityTooltipText: {
|
||||
type: String,
|
||||
default: '密度',
|
||||
},
|
||||
columnSettingTooltipText: {
|
||||
type: String,
|
||||
default: '列设置',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'page-change', current: number): void
|
||||
(e: 'refresh'): void
|
||||
(e: 'download'): void
|
||||
(e: 'density-change', size: SizeProps): void
|
||||
(e: 'column-change', columns: TableColumnData[]): void
|
||||
}>()
|
||||
|
||||
const size = ref<SizeProps>('medium')
|
||||
const cloneColumns = ref<Column[]>([])
|
||||
const showColumns = ref<Column[]>([])
|
||||
const columnSettingRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// Sortable 实例缓存
|
||||
let sortableInstance: Sortable | null = null
|
||||
|
||||
// 密度列表
|
||||
const densityList = [
|
||||
{ name: '迷你', value: 'mini' as SizeProps },
|
||||
{ name: '偏小', value: 'small' as SizeProps },
|
||||
{ name: '中等', value: 'medium' as SizeProps },
|
||||
{ name: '偏大', value: 'large' as SizeProps },
|
||||
]
|
||||
|
||||
// 计算需要插槽的列(只在 columns 变化时重新计算)
|
||||
const slotColumns = computed(() => {
|
||||
return props.columns.filter(col => col.slotName)
|
||||
})
|
||||
|
||||
const onPageChange = (current: number) => {
|
||||
emit('page-change', current)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
emit('download')
|
||||
}
|
||||
|
||||
const handleSelectDensity = (val: string | number | Record<string, any> | undefined) => {
|
||||
size.value = val as SizeProps
|
||||
emit('density-change', size.value)
|
||||
}
|
||||
|
||||
const handleChange = (checked: boolean | (string | boolean | number)[], column: Column, index: number) => {
|
||||
if (!checked) {
|
||||
cloneColumns.value = showColumns.value.filter((item) => item.dataIndex !== column.dataIndex)
|
||||
} else {
|
||||
cloneColumns.value.splice(index, 0, column)
|
||||
}
|
||||
emit('column-change', cloneColumns.value)
|
||||
}
|
||||
|
||||
const exchangeArray = <T extends Array<any>>(array: T, beforeIdx: number, newIdx: number): T => {
|
||||
if (beforeIdx > -1 && newIdx > -1 && beforeIdx !== newIdx) {
|
||||
const temp = array[beforeIdx]
|
||||
array.splice(beforeIdx, 1)
|
||||
array.splice(newIdx, 0, temp)
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
const popupVisibleChange = (val: boolean) => {
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
if (columnSettingRef.value && !sortableInstance) {
|
||||
sortableInstance = new Sortable(columnSettingRef.value, {
|
||||
animation: 150,
|
||||
onEnd(e: any) {
|
||||
const { oldIndex, newIndex } = e
|
||||
if (oldIndex !== undefined && newIndex !== undefined) {
|
||||
exchangeArray(cloneColumns.value, oldIndex, newIndex)
|
||||
exchangeArray(showColumns.value, oldIndex, newIndex)
|
||||
emit('column-change', cloneColumns.value)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化列配置
|
||||
const initColumns = () => {
|
||||
const cols = props.columns.map(item => ({
|
||||
...item,
|
||||
checked: true,
|
||||
}))
|
||||
cloneColumns.value = cols
|
||||
showColumns.value = cloneDeep(cols)
|
||||
}
|
||||
|
||||
// 监听列配置变化
|
||||
watch(
|
||||
() => props.columns,
|
||||
(val, oldVal) => {
|
||||
if (val !== oldVal || cloneColumns.value.length === 0) {
|
||||
initColumns()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 组件卸载时销毁 Sortable 实例
|
||||
onUnmounted(() => {
|
||||
if (sortableInstance) {
|
||||
sortableInstance.destroy()
|
||||
sortableInstance = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DataTable',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.data-table {
|
||||
:deep(.arco-table-th) {
|
||||
&:last-child {
|
||||
.arco-table-th-item-title {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.action-icon {
|
||||
margin-left: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.active {
|
||||
color: #0960bd;
|
||||
background-color: #e3f4fc;
|
||||
}
|
||||
.column-setting-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 200px;
|
||||
.title {
|
||||
margin-left: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,9 @@ import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { App } from 'vue'
|
||||
import Breadcrumb from './breadcrumb/index.vue'
|
||||
import Chart from './chart/index.vue'
|
||||
import SearchForm from './search-form/index.vue'
|
||||
import DataTable from './data-table/index.vue'
|
||||
import SearchTable from './search-table/index.vue'
|
||||
|
||||
// Manually introduce ECharts modules to reduce packing size
|
||||
|
||||
@@ -25,5 +28,8 @@ export default {
|
||||
install(Vue: App) {
|
||||
Vue.component('Chart', Chart)
|
||||
Vue.component('Breadcrumb', Breadcrumb)
|
||||
Vue.component('SearchForm', SearchForm)
|
||||
Vue.component('DataTable', DataTable)
|
||||
Vue.component('SearchTable', SearchTable)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
import { RouteRecordRaw, useRoute, useRouter } from 'vue-router'
|
||||
import useMenuTree from './use-menu-tree'
|
||||
import { COMMON_ICONS } from '@/views/ops/pages/system-settings/menu-management/menuIcons'
|
||||
|
||||
export default defineComponent({
|
||||
emit: ['collapse'],
|
||||
@@ -85,12 +86,28 @@ export default defineComponent({
|
||||
if (appStore.device === 'desktop') appStore.updateSettings({ menuCollapse: val })
|
||||
}
|
||||
|
||||
// 获取图标组件 - 支持 Arco Design 图标和 @tabler/icons-vue
|
||||
const getIconComponent = (iconName: string) => {
|
||||
if (!iconName) return null
|
||||
|
||||
// 检查是否是 Tabler 图标(不以 'icon-' 开头)
|
||||
if (!iconName.startsWith('icon-')) {
|
||||
const IconComponent = COMMON_ICONS[iconName]
|
||||
if (IconComponent) {
|
||||
return () => h(IconComponent, { size: 18 })
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到 Arco Design 图标
|
||||
return () => h(compile(`<${iconName}/>`))
|
||||
}
|
||||
|
||||
const renderSubMenu = () => {
|
||||
function travel(_route: RouteRecordRaw[], nodes = []) {
|
||||
if (_route) {
|
||||
_route.forEach((element) => {
|
||||
// This is demo, modify nodes as needed
|
||||
const icon = element?.meta?.icon ? () => h(compile(`<${element?.meta?.icon}/>`)) : null
|
||||
const icon = element?.meta?.icon ? getIconComponent(element?.meta?.icon as string) : null
|
||||
const node =
|
||||
element?.children && element?.children.length !== 0 ? (
|
||||
<a-sub-menu
|
||||
|
||||
136
src/components/search-form/index.vue
Normal file
136
src/components/search-form/index.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<a-row>
|
||||
<a-col :flex="1">
|
||||
<a-form
|
||||
:model="localModel"
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 18 }"
|
||||
label-align="left"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col v-for="item in formItems" :key="item.field" :span="item.span || 8">
|
||||
<a-form-item :field="item.field" :label="item.label">
|
||||
<!-- 输入框 -->
|
||||
<a-input
|
||||
v-if="item.type === 'input'"
|
||||
v-model="localModel[item.field]"
|
||||
:placeholder="item.placeholder"
|
||||
allow-clear
|
||||
/>
|
||||
<!-- 选择框 -->
|
||||
<a-select
|
||||
v-else-if="item.type === 'select'"
|
||||
v-model="localModel[item.field]"
|
||||
:options="item.options"
|
||||
:placeholder="item.placeholder || '请选择'"
|
||||
allow-clear
|
||||
/>
|
||||
<!-- 日期范围选择器 -->
|
||||
<a-range-picker
|
||||
v-else-if="item.type === 'dateRange'"
|
||||
v-model="localModel[item.field]"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-col>
|
||||
<a-divider v-if="showButtons" style="height: 84px" direction="vertical" />
|
||||
<a-col v-if="showButtons" :flex="'86px'" style="text-align: right">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon>
|
||||
<icon-search />
|
||||
</template>
|
||||
{{ searchButtonText }}
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
{{ resetButtonText }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType, reactive, watch } from 'vue'
|
||||
import type { FormItem } from './types'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
required: true,
|
||||
},
|
||||
formItems: {
|
||||
type: Array as PropType<FormItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
showButtons: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
searchButtonText: {
|
||||
type: String,
|
||||
default: '查询',
|
||||
},
|
||||
resetButtonText: {
|
||||
type: String,
|
||||
default: '重置',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search'): void
|
||||
(e: 'reset'): void
|
||||
}>()
|
||||
|
||||
// 使用本地响应式副本,避免直接修改 props
|
||||
const localModel = reactive<Record<string, any>>({})
|
||||
|
||||
// 初始化本地模型
|
||||
const initLocalModel = () => {
|
||||
Object.keys(localModel).forEach(key => delete localModel[key])
|
||||
Object.assign(localModel, props.modelValue)
|
||||
}
|
||||
|
||||
// 监听外部值变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
Object.keys(localModel).forEach(key => delete localModel[key])
|
||||
Object.assign(localModel, val)
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
// 监听本地值变化,同步到外部
|
||||
watch(
|
||||
localModel,
|
||||
(val) => {
|
||||
emit('update:modelValue', { ...val })
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const handleSearch = () => {
|
||||
emit('search')
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SearchForm',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
</style>
|
||||
10
src/components/search-form/types.ts
Normal file
10
src/components/search-form/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface'
|
||||
|
||||
export interface FormItem {
|
||||
field: string
|
||||
label: string
|
||||
type: 'input' | 'select' | 'dateRange' | 'slot'
|
||||
span?: number
|
||||
placeholder?: string
|
||||
options?: SelectOptionData[]
|
||||
}
|
||||
217
src/components/search-table/index.vue
Normal file
217
src/components/search-table/index.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="search-table-container">
|
||||
<a-card class="general-card" :title="title">
|
||||
<!-- 搜索表单 -->
|
||||
<SearchForm
|
||||
:model-value="formModel"
|
||||
:form-items="formItems"
|
||||
:show-buttons="showSearchButtons"
|
||||
:search-button-text="searchButtonText"
|
||||
:reset-button-text="resetButtonText"
|
||||
@update:model-value="handleFormModelUpdate"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
/>
|
||||
|
||||
<a-divider style="margin-top: 0" />
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<DataTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:bordered="bordered"
|
||||
:show-toolbar="showToolbar"
|
||||
:show-download="showDownload"
|
||||
:show-refresh="showRefresh"
|
||||
:show-density="showDensity"
|
||||
:show-column-setting="showColumnSetting"
|
||||
:download-button-text="downloadButtonText"
|
||||
:refresh-tooltip-text="refreshTooltipText"
|
||||
:density-tooltip-text="densityTooltipText"
|
||||
:column-setting-tooltip-text="columnSettingTooltipText"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
@download="handleDownload"
|
||||
@density-change="handleDensityChange"
|
||||
@column-change="handleColumnChange"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<slot name="toolbar-left" />
|
||||
</template>
|
||||
<template #toolbar-right>
|
||||
<slot name="toolbar-right" />
|
||||
</template>
|
||||
<!-- 动态插槽透传 -->
|
||||
<template v-for="col in slotColumns" :key="col.dataIndex" #[String(col.slotName)]="slotProps">
|
||||
<slot :name="col.slotName" v-bind="slotProps" />
|
||||
</template>
|
||||
</DataTable>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, PropType } from 'vue'
|
||||
import SearchForm from '../search-form/index.vue'
|
||||
import type { FormItem } from '../search-form/types'
|
||||
import DataTable from '../data-table/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
type SizeProps = 'mini' | 'small' | 'medium' | 'large'
|
||||
|
||||
const props = defineProps({
|
||||
// 表单相关
|
||||
formModel: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
required: true,
|
||||
},
|
||||
formItems: {
|
||||
type: Array as PropType<FormItem[]>,
|
||||
default: () => [],
|
||||
},
|
||||
showSearchButtons: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
searchButtonText: {
|
||||
type: String,
|
||||
default: '查询',
|
||||
},
|
||||
resetButtonText: {
|
||||
type: String,
|
||||
default: '重置',
|
||||
},
|
||||
// 表格相关
|
||||
data: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: () => [],
|
||||
},
|
||||
columns: {
|
||||
type: Array as PropType<TableColumnData[]>,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
pagination: {
|
||||
type: Object as PropType<{
|
||||
current: number
|
||||
pageSize: number
|
||||
total?: number
|
||||
}>,
|
||||
default: () => ({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
}),
|
||||
},
|
||||
bordered: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 工具栏相关
|
||||
showToolbar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showDownload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showRefresh: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showDensity: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showColumnSetting: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 文本配置
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
downloadButtonText: {
|
||||
type: String,
|
||||
default: '下载',
|
||||
},
|
||||
refreshTooltipText: {
|
||||
type: String,
|
||||
default: '刷新',
|
||||
},
|
||||
densityTooltipText: {
|
||||
type: String,
|
||||
default: '密度',
|
||||
},
|
||||
columnSettingTooltipText: {
|
||||
type: String,
|
||||
default: '列设置',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:formModel', value: Record<string, any>): void
|
||||
(e: 'search'): void
|
||||
(e: 'reset'): void
|
||||
(e: 'page-change', current: number): void
|
||||
(e: 'refresh'): void
|
||||
(e: 'download'): void
|
||||
(e: 'density-change', size: SizeProps): void
|
||||
(e: 'column-change', columns: TableColumnData[]): void
|
||||
}>()
|
||||
|
||||
// 计算需要插槽的列(动态插槽透传)
|
||||
const slotColumns = computed(() => {
|
||||
return props.columns.filter(col => col.slotName)
|
||||
})
|
||||
|
||||
const handleFormModelUpdate = (value: Record<string, any>) => {
|
||||
emit('update:formModel', value)
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
emit('search')
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
const handlePageChange = (current: number) => {
|
||||
emit('page-change', current)
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
emit('download')
|
||||
}
|
||||
|
||||
const handleDensityChange = (size: SizeProps) => {
|
||||
emit('density-change', size)
|
||||
}
|
||||
|
||||
const handleColumnChange = (columns: TableColumnData[]) => {
|
||||
emit('column-change', columns)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SearchTable',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.search-table-container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -12,6 +12,6 @@
|
||||
"globalSettings": false,
|
||||
"device": "desktop",
|
||||
"tabBar": true,
|
||||
"menuFromServer": false,
|
||||
"menuFromServer": true,
|
||||
"serverMenu": []
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import localeWorkplace from '@/views/dashboard/workplace/locale/en-US'
|
||||
/** simple */
|
||||
import localeMonitor from '@/views/dashboard/monitor/locale/en-US'
|
||||
|
||||
import localeCardList from '@/views/list/card/locale/en-US'
|
||||
import localeSearchTable from '@/views/list/search-table/locale/en-US'
|
||||
|
||||
import localeBasicProfile from '@/views/profile/basic/locale/en-US'
|
||||
@@ -17,8 +16,6 @@ import locale403 from '@/views/exception/403/locale/en-US'
|
||||
import locale404 from '@/views/exception/404/locale/en-US'
|
||||
import locale500 from '@/views/exception/500/locale/en-US'
|
||||
|
||||
import localeUserInfo from '@/views/user/info/locale/en-US'
|
||||
import localeUserSetting from '@/views/user/setting/locale/en-US'
|
||||
/** simple end */
|
||||
import localeSettings from './en-US/settings'
|
||||
|
||||
@@ -28,6 +25,57 @@ export default {
|
||||
'menu.server.workplace': 'Workplace-Server',
|
||||
'menu.server.monitor': 'Monitor-Server',
|
||||
'menu.list': 'List',
|
||||
'menu.ops': 'Operations',
|
||||
'menu.ops.systemSettings': 'System Settings',
|
||||
'menu.ops.systemSettings.menuManagement': 'Menu Management',
|
||||
'menu.ops.systemSettings.systemLogs': 'System Logs',
|
||||
'menu.management': 'Menu Management',
|
||||
'menu.addRoot': 'Add Root Menu',
|
||||
'menu.tip': 'Click a menu item to edit, hover to show action buttons',
|
||||
'menu.empty': 'No menu data',
|
||||
'menu.selectTip': 'Please select a menu item from the left to edit',
|
||||
'menu.group': 'Group',
|
||||
'menu.addChild': 'Add Child Menu',
|
||||
'menu.delete': 'Delete',
|
||||
'menu.edit': 'Edit',
|
||||
'menu.addChildFor': 'Add child for',
|
||||
'menu.add': 'Add',
|
||||
'menu.details': 'Details',
|
||||
'menu.parent': 'Parent Menu',
|
||||
'menu.deleteConfirm': 'Delete Confirmation',
|
||||
'menu.deleteConfirmText': 'Are you sure you want to delete menu "{name}"?',
|
||||
'menu.deleteChildrenWarning': 'Warning: This menu has child menus, they will also be deleted!',
|
||||
'menu.deleteSuccess': 'Deleted successfully',
|
||||
'menu.saveSuccess': 'Saved successfully',
|
||||
'menu.basicInfo': 'Basic Information',
|
||||
'menu.code': 'Menu Code',
|
||||
'menu.codeRequired': 'Please enter menu code',
|
||||
'menu.codePlaceholder': 'Enter menu code, e.g.: dashboard',
|
||||
'menu.type': 'Menu Type',
|
||||
'menu.submenu': 'Submenu',
|
||||
'menu.rootMenu': 'Root Menu',
|
||||
'menu.title': 'Menu Title',
|
||||
'menu.titleRequired': 'Please enter menu title',
|
||||
'menu.titlePlaceholder': 'Enter Chinese title',
|
||||
'menu.titleEn': 'English Title',
|
||||
'menu.titleEnRequired': 'Please enter English title',
|
||||
'menu.titleEnPlaceholder': 'Enter English title',
|
||||
'menu.path': 'Menu Path',
|
||||
'menu.pathRequired': 'Please enter menu path',
|
||||
'menu.pathPlaceholder': 'Enter menu path, e.g.: /dashboard',
|
||||
'menu.icon': 'Menu Icon',
|
||||
'menu.iconRequired': 'Please select menu icon',
|
||||
'menu.iconPlaceholder': 'Click to select icon',
|
||||
'menu.description': 'Description',
|
||||
'menu.descriptionPlaceholder': 'Enter menu description',
|
||||
'menu.webConfig': 'Web Page Configuration',
|
||||
'menu.isWebPage': 'Is Embedded Web Page',
|
||||
'menu.webUrl': 'Web URL',
|
||||
'menu.webUrlPlaceholder': 'Enter the URL to embed',
|
||||
'menu.save': 'Save',
|
||||
'menu.cancel': 'Cancel',
|
||||
'menu.selectIcon': 'Select Icon',
|
||||
'menu.iconSearch': 'Search icon',
|
||||
'menu.result': 'Result',
|
||||
'menu.exception': 'Exception',
|
||||
'menu.profile': 'Profile',
|
||||
@@ -44,14 +92,11 @@ export default {
|
||||
/** simple */
|
||||
...localeMonitor,
|
||||
...localeSearchTable,
|
||||
...localeCardList,
|
||||
...localeBasicProfile,
|
||||
...localeDataAnalysis,
|
||||
...localeMultiDAnalysis,
|
||||
...locale403,
|
||||
...locale404,
|
||||
...locale500,
|
||||
...localeUserInfo,
|
||||
...localeUserSetting,
|
||||
/** simple end */
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import localeWorkplace from '@/views/dashboard/workplace/locale/zh-CN'
|
||||
/** simple */
|
||||
import localeMonitor from '@/views/dashboard/monitor/locale/zh-CN'
|
||||
|
||||
import localeCardList from '@/views/list/card/locale/zh-CN'
|
||||
import localeSearchTable from '@/views/list/search-table/locale/zh-CN'
|
||||
|
||||
import localeBasicProfile from '@/views/profile/basic/locale/zh-CN'
|
||||
@@ -17,8 +16,6 @@ import locale403 from '@/views/exception/403/locale/zh-CN'
|
||||
import locale404 from '@/views/exception/404/locale/zh-CN'
|
||||
import locale500 from '@/views/exception/500/locale/zh-CN'
|
||||
|
||||
import localeUserInfo from '@/views/user/info/locale/zh-CN'
|
||||
import localeUserSetting from '@/views/user/setting/locale/zh-CN'
|
||||
/** simple end */
|
||||
import localeSettings from './zh-CN/settings'
|
||||
|
||||
@@ -28,6 +25,57 @@ export default {
|
||||
'menu.server.workplace': '工作台-服务端',
|
||||
'menu.server.monitor': '实时监控-服务端',
|
||||
'menu.list': '列表页',
|
||||
'menu.ops': '运维管理',
|
||||
'menu.ops.systemSettings': '系统设置',
|
||||
'menu.ops.systemSettings.menuManagement': '菜单管理',
|
||||
'menu.ops.systemSettings.systemLogs': '系统日志',
|
||||
'menu.management': '菜单管理',
|
||||
'menu.addRoot': '添加根菜单',
|
||||
'menu.tip': '点击菜单项可编辑,悬停显示操作按钮',
|
||||
'menu.empty': '暂无菜单数据',
|
||||
'menu.selectTip': '请从左侧选择一个菜单项进行编辑',
|
||||
'menu.group': '分组',
|
||||
'menu.addChild': '添加子菜单',
|
||||
'menu.delete': '删除',
|
||||
'menu.edit': '编辑',
|
||||
'menu.addChildFor': '为',
|
||||
'menu.add': '添加',
|
||||
'menu.details': '详情',
|
||||
'menu.parent': '父菜单',
|
||||
'menu.deleteConfirm': '删除确认',
|
||||
'menu.deleteConfirmText': '确定要删除菜单 "{name}" 吗?',
|
||||
'menu.deleteChildrenWarning': '注意:该菜单下有子菜单,删除后子菜单也会被删除!',
|
||||
'menu.deleteSuccess': '删除成功',
|
||||
'menu.saveSuccess': '保存成功',
|
||||
'menu.basicInfo': '基本信息',
|
||||
'menu.code': '菜单编码',
|
||||
'menu.codeRequired': '请输入菜单编码',
|
||||
'menu.codePlaceholder': '请输入菜单编码,如:dashboard',
|
||||
'menu.type': '菜单类型',
|
||||
'menu.submenu': '子菜单',
|
||||
'menu.rootMenu': '根菜单',
|
||||
'menu.title': '菜单标题',
|
||||
'menu.titleRequired': '请输入菜单标题',
|
||||
'menu.titlePlaceholder': '请输入中文标题',
|
||||
'menu.titleEn': '英文标题',
|
||||
'menu.titleEnRequired': '请输入英文标题',
|
||||
'menu.titleEnPlaceholder': '请输入英文标题',
|
||||
'menu.path': '菜单路径',
|
||||
'menu.pathRequired': '请输入菜单路径',
|
||||
'menu.pathPlaceholder': '请输入菜单路径,如:/dashboard',
|
||||
'menu.icon': '菜单图标',
|
||||
'menu.iconRequired': '请选择菜单图标',
|
||||
'menu.iconPlaceholder': '点击选择图标',
|
||||
'menu.description': '描述',
|
||||
'menu.descriptionPlaceholder': '请输入菜单描述',
|
||||
'menu.webConfig': '网页嵌入配置',
|
||||
'menu.isWebPage': '是否为嵌入网页',
|
||||
'menu.webUrl': '网页地址',
|
||||
'menu.webUrlPlaceholder': '请输入要嵌入的网页URL',
|
||||
'menu.save': '保存',
|
||||
'menu.cancel': '取消',
|
||||
'menu.selectIcon': '选择图标',
|
||||
'menu.iconSearch': '搜索图标',
|
||||
'menu.result': '结果页',
|
||||
'menu.exception': '异常页',
|
||||
'menu.profile': '详情页',
|
||||
@@ -44,14 +92,11 @@ export default {
|
||||
/** simple */
|
||||
...localeMonitor,
|
||||
...localeSearchTable,
|
||||
...localeCardList,
|
||||
...localeBasicProfile,
|
||||
...localeDataAnalysis,
|
||||
...localeMultiDAnalysis,
|
||||
...locale403,
|
||||
...locale404,
|
||||
...locale500,
|
||||
...localeUserInfo,
|
||||
...localeUserSetting,
|
||||
/** simple end */
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import '@/views/dashboard/workplace/mock'
|
||||
/** simple */
|
||||
import '@/views/dashboard/monitor/mock'
|
||||
|
||||
import '@/views/list/card/mock'
|
||||
import '@/views/list/search-table/mock'
|
||||
|
||||
import '@/views/profile/basic/mock'
|
||||
@@ -15,8 +14,6 @@ import '@/views/profile/basic/mock'
|
||||
import '@/views/visualization/data-analysis/mock'
|
||||
import '@/views/visualization/multi-dimension-data-analysis/mock'
|
||||
|
||||
import '@/views/user/info/mock'
|
||||
import '@/views/user/setting/mock'
|
||||
/** simple end */
|
||||
|
||||
Mock.setup({
|
||||
|
||||
932
src/router/local-menu-flat.ts
Normal file
932
src/router/local-menu-flat.ts
Normal file
@@ -0,0 +1,932 @@
|
||||
import type { MenuItem } from '@/api/module/pmn'
|
||||
|
||||
// 本地菜单数据(扁平格式)- 接口未准备好时使用
|
||||
// 这是从 fetchMenu 接口返回的原始扁平数据格式
|
||||
export const localMenuFlatItems: MenuItem[] = [
|
||||
{
|
||||
id: 8,
|
||||
identity: '019b591d-0064-7dcc-9c78-59b5f4aa31a7',
|
||||
title: '系统概况',
|
||||
title_en: 'Home',
|
||||
code: 'ops:系统概况',
|
||||
description: '首页菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/overview',
|
||||
menu_icon: 'Home',
|
||||
type: 1,
|
||||
sort_key: 1,
|
||||
created_at: '2025-12-26T13:23:51.54067+08:00',
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
identity: '019b591d-00c3-7955-aa1b-80b5a0c8d6bd',
|
||||
title: '可视化大屏管理',
|
||||
title_en: 'Visual Dashboard Management',
|
||||
code: 'ops:可视化大屏管理',
|
||||
description: '可视化大屏管理菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/visual/',
|
||||
menu_icon: 'Virus',
|
||||
type: 1,
|
||||
sort_key: 2,
|
||||
created_at: '2025-12-26T13:23:51.62748+08:00',
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
identity: '019b591d-00dc-7486-aa93-51e798d3253a',
|
||||
title: '大屏管理',
|
||||
title_en: 'My Components',
|
||||
code: 'ops:可视化大屏管理:我的组件',
|
||||
description: '可视化大屏管理 - 我的组件',
|
||||
app_id: 2,
|
||||
parent_id: 13,
|
||||
menu_path: '/visual/component',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 3,
|
||||
is_web_page: true,
|
||||
web_url: 'https://ops.apinb.com/view/#/project/items',
|
||||
created_at: '2025-12-26T13:23:51.644296+08:00',
|
||||
},
|
||||
{
|
||||
id: 85,
|
||||
identity: '019bf309-a961-7bba-a03d-83eb78a82437',
|
||||
title: '已发布大屏列表',
|
||||
title_en: 'View Publish',
|
||||
code: '已发布大屏列表',
|
||||
app_id: 2,
|
||||
parent_id: 13,
|
||||
menu_path: '/visual/view-publish',
|
||||
type: 1,
|
||||
sort_key: 4,
|
||||
is_web_page: true,
|
||||
web_url: 'https://ops.apinb.com/view/#/project/management',
|
||||
created_at: '2026-01-25T10:44:15.33024+08:00',
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
identity: '019b591d-00f4-73a0-bbdb-aa7da79ed390',
|
||||
title: '服务器及PC管理',
|
||||
title_en: '服务器及pc管理',
|
||||
code: 'ops:服务器及pc管理',
|
||||
description: '服务器及PC管理菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/dc/',
|
||||
menu_icon: 'Building',
|
||||
type: 1,
|
||||
sort_key: 5,
|
||||
created_at: '2025-12-26T13:23:51.675908+08:00',
|
||||
},
|
||||
{
|
||||
id: 18,
|
||||
identity: '019b591d-011a-7dca-b85c-4802ad0e5d05',
|
||||
title: '办公PC管理',
|
||||
title_en: '办公pc管理',
|
||||
code: 'ops:服务器及pc管理:办公pc管理',
|
||||
description: '服务器及PC管理 - 办公PC管理',
|
||||
app_id: 2,
|
||||
parent_id: 16,
|
||||
menu_path: '/dc/pc',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 6,
|
||||
created_at: '2025-12-26T13:23:51.706903+08:00',
|
||||
},
|
||||
{
|
||||
id: 17,
|
||||
identity: '019b591d-010b-790f-adbc-401ee42c20ae',
|
||||
title: '服务器管理',
|
||||
title_en: '服务器管理',
|
||||
code: 'ops:服务器及pc管理:服务器管理',
|
||||
description: '服务器及PC管理 - 服务器管理',
|
||||
app_id: 2,
|
||||
parent_id: 16,
|
||||
menu_path: '/dc/server',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 7,
|
||||
created_at: '2025-12-26T13:23:51.691593+08:00',
|
||||
},
|
||||
{
|
||||
id: 19,
|
||||
identity: '019b591d-0122-7959-a3c3-f5226d0bf278',
|
||||
title: '集群采集控制中心',
|
||||
title_en: 'Cluster Collection Control Center',
|
||||
code: 'ops:集群采集控制中心',
|
||||
description: '集群采集控制中心菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/dc/',
|
||||
menu_icon: 'Adjustments',
|
||||
type: 1,
|
||||
sort_key: 8,
|
||||
created_at: '2025-12-26T13:23:51.715137+08:00',
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
identity: '019b591d-013a-7c89-be2d-5f03e5d7c5da',
|
||||
title: '数据库采集管理',
|
||||
title_en: 'Database Collection Management',
|
||||
code: 'ops:集群采集控制中心:数据库采集管理',
|
||||
description: '集群采集控制中心 - 数据库采集管理',
|
||||
app_id: 2,
|
||||
parent_id: 19,
|
||||
menu_path: '/dc/database',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 9,
|
||||
created_at: '2025-12-26T13:23:51.738821+08:00',
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
identity: '019b591d-014a-710a-8b3f-30713e516d70',
|
||||
title: '中间件采集管理',
|
||||
title_en: 'Middleware Collection Management',
|
||||
code: 'ops:集群采集控制中心:中间件采集管理',
|
||||
description: '集群采集控制中心 - 中间件采集管理',
|
||||
app_id: 2,
|
||||
parent_id: 19,
|
||||
menu_path: '/dc/middleware',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 10,
|
||||
created_at: '2025-12-26T13:23:51.754068+08:00',
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
identity: '019b591d-0159-7e46-a5e0-fb69c6b62a25',
|
||||
title: '网络设备采集管理',
|
||||
title_en: 'Network Device Collection Management',
|
||||
code: 'ops:集群采集控制中心:网络设备采集管理',
|
||||
description: '集群采集控制中心 - 网络设备采集管理',
|
||||
app_id: 2,
|
||||
parent_id: 19,
|
||||
menu_path: '/dc/network',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 11,
|
||||
created_at: '2025-12-26T13:23:51.769935+08:00',
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
identity: '019b591d-0160-736e-a42d-180fef3fb3be',
|
||||
title: '综合监控',
|
||||
title_en: 'Comprehensive Monitoring',
|
||||
code: 'ops:综合监控',
|
||||
description: '综合监控菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/monitor/',
|
||||
menu_icon: 'Desktop',
|
||||
type: 1,
|
||||
sort_key: 12,
|
||||
created_at: '2025-12-26T13:23:51.77834+08:00',
|
||||
},
|
||||
{
|
||||
id: 31,
|
||||
identity: '019b591d-01e3-7adc-b10f-26550a6e3700',
|
||||
title: '日志监控',
|
||||
title_en: 'Log Monitoring',
|
||||
code: 'ops:综合监控:日志监控',
|
||||
description: '综合监控 - 日志监控',
|
||||
app_id: 2,
|
||||
parent_id: 23,
|
||||
menu_path: '/monitor/log',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 13,
|
||||
created_at: '2025-12-26T13:23:51.907711+08:00',
|
||||
},
|
||||
{
|
||||
id: 30,
|
||||
identity: '019b591d-01d4-78b0-89ea-a6ba12914379',
|
||||
title: '虚拟化监控',
|
||||
title_en: 'Virtualization Monitoring',
|
||||
code: 'ops:综合监控:虚拟化监控',
|
||||
description: '综合监控 - 虚拟化监控',
|
||||
app_id: 2,
|
||||
parent_id: 23,
|
||||
menu_path: '/monitor/virtualization',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 14,
|
||||
created_at: '2025-12-26T13:23:51.892569+08:00',
|
||||
},
|
||||
{
|
||||
id: 27,
|
||||
identity: '019b591d-01a5-776f-ac4b-3cd896dd3f48',
|
||||
title: '存储设备监控',
|
||||
title_en: 'Storage Device Monitoring',
|
||||
code: 'ops:综合监控:存储设备监控',
|
||||
description: '综合监控 - 存储设备监控',
|
||||
app_id: 2,
|
||||
parent_id: 23,
|
||||
menu_path: '/monitor/storage',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 15,
|
||||
created_at: '2025-12-26T13:23:51.845487+08:00',
|
||||
},
|
||||
{
|
||||
id: 25,
|
||||
identity: '019b591d-0187-7a06-888f-0e924c11544c',
|
||||
title: '网络设备监控',
|
||||
title_en: 'Network Device Monitoring',
|
||||
code: 'ops:综合监控:网络设备监控',
|
||||
description: '综合监控 - 网络设备监控',
|
||||
app_id: 2,
|
||||
parent_id: 23,
|
||||
menu_path: '/monitor/network',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 16,
|
||||
created_at: '2025-12-26T13:23:51.815656+08:00',
|
||||
},
|
||||
{
|
||||
id: 33,
|
||||
identity: '019b591d-0202-7c99-b768-68f674b1daaa',
|
||||
title: '电力/UPS/空调/温湿度',
|
||||
title_en: 'Power/UPS/AC/Temperature',
|
||||
code: 'ops:综合监控:电力_ups_空调_温湿度',
|
||||
description: '综合监控 - 电力/UPS/空调/温湿度',
|
||||
app_id: 2,
|
||||
parent_id: 23,
|
||||
menu_path: '/monitor/power',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 17,
|
||||
created_at: '2025-12-26T13:23:51.938825+08:00',
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
identity: '019b591d-01f3-7698-9aad-9f9dbb3be203',
|
||||
title: 'URL监控',
|
||||
title_en: 'URL Monitoring',
|
||||
code: 'ops:综合监控:url监控',
|
||||
description: '综合监控 - URL监控',
|
||||
app_id: 2,
|
||||
parent_id: 23,
|
||||
menu_path: '/monitor/url',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 18,
|
||||
created_at: '2025-12-26T13:23:51.923432+08:00',
|
||||
},
|
||||
{
|
||||
id: 34,
|
||||
identity: '019b591d-0210-7e32-ad40-ebbf2f48282f',
|
||||
title: '消防/门禁/漏水/有害气体',
|
||||
title_en: 'Fire/Access/Leakage/Gas',
|
||||
code: 'ops:综合监控:消防_门禁_漏水_有害气体',
|
||||
description: '综合监控 - 消防/门禁/漏水/有害气体',
|
||||
app_id: 2,
|
||||
parent_id: 23,
|
||||
menu_path: '/monitor/guard',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 19,
|
||||
created_at: '2025-12-26T13:23:51.95293+08:00',
|
||||
},
|
||||
{
|
||||
id: 26,
|
||||
identity: '019b591d-0194-7bde-8437-1cd85a507548',
|
||||
title: '安全设备监控',
|
||||
title_en: 'Security Device Monitoring',
|
||||
code: 'ops:综合监控:安全设备监控',
|
||||
description: '综合监控 - 安全设备监控',
|
||||
app_id: 2,
|
||||
parent_id: 23,
|
||||
menu_path: '/monitor/security',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 20,
|
||||
created_at: '2025-12-26T13:23:51.828777+08:00',
|
||||
},
|
||||
{
|
||||
id: 35,
|
||||
identity: '019b591d-021a-74a3-8092-c15b990f3c7e',
|
||||
title: '网络架构管理',
|
||||
title_en: 'Network Architecture Management',
|
||||
code: 'ops:网络架构管理',
|
||||
description: '网络架构管理菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/netarch/',
|
||||
menu_icon: 'Laptop',
|
||||
type: 1,
|
||||
sort_key: 21,
|
||||
created_at: '2025-12-26T13:23:51.969818+08:00',
|
||||
},
|
||||
{
|
||||
id: 83,
|
||||
identity: '019b8e92-3714-7376-936c-570b015b653b',
|
||||
title: '自动感知拓扑图',
|
||||
title_en: 'Auto Topo',
|
||||
code: 'AutoTopo',
|
||||
app_id: 2,
|
||||
parent_id: 35,
|
||||
menu_path: '/netarch/auto-topo',
|
||||
type: 1,
|
||||
sort_key: 22,
|
||||
created_at: '2026-01-05T22:31:45.684645+08:00',
|
||||
},
|
||||
{
|
||||
id: 36,
|
||||
identity: '019b591d-0231-7667-a9fc-cfeb05da5aab',
|
||||
title: '拓扑管理',
|
||||
title_en: 'Topology Management',
|
||||
code: 'ops:网络架构管理:拓扑管理',
|
||||
description: '网络架构管理 - 拓扑管理',
|
||||
app_id: 2,
|
||||
parent_id: 35,
|
||||
menu_path: '/netarch/topo-group',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 23,
|
||||
created_at: '2025-12-26T13:23:51.985419+08:00',
|
||||
},
|
||||
{
|
||||
id: 37,
|
||||
identity: '019b591d-0240-7d6d-90b8-a0a6303665dc',
|
||||
title: '流量分析管理',
|
||||
title_en: 'Traffic Analysis Management',
|
||||
code: 'ops:网络架构管理:流量分析管理',
|
||||
description: '网络架构管理 - 流量分析管理',
|
||||
app_id: 2,
|
||||
parent_id: 35,
|
||||
menu_path: '/netarch/traffic',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 24,
|
||||
created_at: '2025-12-26T13:23:52.000879+08:00',
|
||||
},
|
||||
{
|
||||
id: 38,
|
||||
identity: '019b591d-024c-7564-942d-cf33e0ed6204',
|
||||
title: 'IP地址管理',
|
||||
title_en: 'IP Address Management',
|
||||
code: 'ops:网络架构管理:ip地址管理',
|
||||
description: '网络架构管理 - IP地址管理',
|
||||
app_id: 2,
|
||||
parent_id: 35,
|
||||
menu_path: '/netarch/ip',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 25,
|
||||
created_at: '2025-12-26T13:23:52.012353+08:00',
|
||||
},
|
||||
{
|
||||
id: 39,
|
||||
identity: '019b591d-0258-7316-9b58-d664678bb3af',
|
||||
title: '告警管理',
|
||||
title_en: 'Alert Management',
|
||||
code: 'ops:告警管理',
|
||||
description: '告警管理菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/alert/',
|
||||
menu_icon: 'ShieldLock',
|
||||
type: 1,
|
||||
sort_key: 26,
|
||||
created_at: '2025-12-26T13:23:52.032311+08:00',
|
||||
},
|
||||
{
|
||||
id: 45,
|
||||
identity: '019b591d-02be-713f-91fe-5f5d668380c8',
|
||||
title: '告警策略管理',
|
||||
title_en: 'Alert Policy Management',
|
||||
code: 'ops:告警管理:告警策略管理',
|
||||
description: '告警管理 - 告警策略管理',
|
||||
app_id: 2,
|
||||
parent_id: 39,
|
||||
menu_path: '/alert/setting',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 27,
|
||||
created_at: '2025-12-26T13:23:52.126081+08:00',
|
||||
},
|
||||
{
|
||||
id: 43,
|
||||
identity: '019b591d-029e-7c52-ac1d-d94263e00f8e',
|
||||
title: '告警受理处理',
|
||||
title_en: 'Alert Handling',
|
||||
code: 'ops:告警管理:告警受理处理',
|
||||
description: '告警管理 - 告警受理处理',
|
||||
app_id: 2,
|
||||
parent_id: 39,
|
||||
menu_path: '/alert/tackle',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 28,
|
||||
created_at: '2025-12-26T13:23:52.094807+08:00',
|
||||
},
|
||||
{
|
||||
id: 44,
|
||||
identity: '019b591d-02ae-7587-9e11-7c3d395ea256',
|
||||
title: '告警历史',
|
||||
title_en: 'Alert History',
|
||||
code: 'ops:告警管理:告警历史',
|
||||
description: '告警管理 - 告警历史',
|
||||
app_id: 2,
|
||||
parent_id: 39,
|
||||
menu_path: '/alert/history',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 29,
|
||||
created_at: '2025-12-26T13:23:52.110362+08:00',
|
||||
},
|
||||
{
|
||||
id: 40,
|
||||
identity: '019b591d-026f-785d-b473-ac804133e251',
|
||||
title: '告警模版',
|
||||
title_en: 'Alert Template',
|
||||
code: 'ops:告警管理:告警模版',
|
||||
description: '告警管理 - 告警模版',
|
||||
app_id: 2,
|
||||
parent_id: 39,
|
||||
menu_path: '/alert/template',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 30,
|
||||
created_at: '2025-12-26T13:23:52.047548+08:00',
|
||||
},
|
||||
{
|
||||
id: 41,
|
||||
identity: '019b591d-027d-7eae-b9b9-23fd1b7ece75',
|
||||
title: '告警通知设置',
|
||||
title_en: 'Alert Notification Settings',
|
||||
code: 'ops:告警管理:告警通知设置',
|
||||
description: '告警管理 - 告警通知设置',
|
||||
app_id: 2,
|
||||
parent_id: 39,
|
||||
menu_path: '/alert/notice',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 31,
|
||||
created_at: '2025-12-26T13:23:52.061962+08:00',
|
||||
},
|
||||
{
|
||||
id: 42,
|
||||
identity: '019b591d-028e-7bb4-acf3-8bcb0c5b4b75',
|
||||
title: '告警级别管理',
|
||||
title_en: 'Alert Level Management',
|
||||
code: 'ops:告警管理:告警级别管理',
|
||||
description: '告警管理 - 告警级别管理',
|
||||
app_id: 2,
|
||||
parent_id: 39,
|
||||
menu_path: '/alert/level',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 32,
|
||||
created_at: '2025-12-26T13:23:52.078767+08:00',
|
||||
},
|
||||
{
|
||||
id: 46,
|
||||
identity: '019b591d-02c5-7ba6-ac39-847b5bbdc9c6',
|
||||
title: '工单管理',
|
||||
title_en: 'Ticket Management',
|
||||
code: 'ops:工单管理',
|
||||
description: '工单管理菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/feedback/',
|
||||
menu_icon: 'Layout',
|
||||
type: 1,
|
||||
sort_key: 33,
|
||||
created_at: '2025-12-26T13:23:52.142086+08:00',
|
||||
},
|
||||
{
|
||||
id: 48,
|
||||
identity: '019b591d-02ec-7e47-adab-c1968a136eae',
|
||||
title: '所有工单',
|
||||
title_en: 'All Tickets',
|
||||
code: 'ops:工单管理:所有工单',
|
||||
description: '工单管理 - 所有工单',
|
||||
app_id: 2,
|
||||
parent_id: 46,
|
||||
menu_path: '/feedback/all',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 34,
|
||||
created_at: '2025-12-26T13:23:52.172935+08:00',
|
||||
},
|
||||
{
|
||||
id: 47,
|
||||
identity: '019b591d-02dd-75c9-b365-097ef1d5bca3',
|
||||
title: '我的工单',
|
||||
title_en: 'Pending Tickets',
|
||||
code: 'ops:工单管理:待处理工单',
|
||||
description: '工单管理 - 待处理工单',
|
||||
app_id: 2,
|
||||
parent_id: 46,
|
||||
menu_path: '/feedback/undo',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 35,
|
||||
created_at: '2025-12-26T13:23:52.157379+08:00',
|
||||
},
|
||||
{
|
||||
id: 49,
|
||||
identity: '019b591d-02f4-7781-8ef4-3993952290d3',
|
||||
title: '数据中心管理',
|
||||
title_en: 'Data Center Management',
|
||||
code: 'ops:数据中心管理',
|
||||
description: '数据中心管理菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/datacenter/',
|
||||
menu_icon: 'Storage',
|
||||
type: 1,
|
||||
sort_key: 36,
|
||||
created_at: '2025-12-26T13:23:52.189288+08:00',
|
||||
},
|
||||
{
|
||||
id: 52,
|
||||
identity: '019b591d-0324-7b16-be4a-d0256a4a66a9',
|
||||
title: '机柜管理',
|
||||
title_en: 'Rack Management',
|
||||
code: 'ops:数据中心管理:机柜管理',
|
||||
description: '数据中心管理 - 机柜管理',
|
||||
app_id: 2,
|
||||
parent_id: 49,
|
||||
menu_path: '/datacenter/rack',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 37,
|
||||
created_at: '2025-12-26T13:23:52.228726+08:00',
|
||||
},
|
||||
{
|
||||
id: 50,
|
||||
identity: '019b591d-0305-75ac-89be-d0c5dd863383',
|
||||
title: '数据中心',
|
||||
title_en: 'Data Center',
|
||||
code: 'ops:数据中心管理:数据中心',
|
||||
description: '数据中心管理 - 数据中心',
|
||||
app_id: 2,
|
||||
parent_id: 49,
|
||||
menu_path: '/datacenter/house',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 38,
|
||||
created_at: '2025-12-26T13:23:52.197371+08:00',
|
||||
},
|
||||
{
|
||||
id: 51,
|
||||
identity: '019b591d-031c-726e-a7ac-0dff6f236d6c',
|
||||
title: '楼层管理',
|
||||
title_en: 'Floor Management',
|
||||
code: 'ops:数据中心管理:楼层管理',
|
||||
description: '数据中心管理 - 楼层管理',
|
||||
app_id: 2,
|
||||
parent_id: 49,
|
||||
menu_path: '/datacenter/floor',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 39,
|
||||
created_at: '2025-12-26T13:23:52.220159+08:00',
|
||||
},
|
||||
{
|
||||
id: 54,
|
||||
identity: '019b591d-0343-7ce7-91bd-d82497ea0a11',
|
||||
title: '资产管理',
|
||||
title_en: 'Asset Management',
|
||||
code: 'ops:资产管理',
|
||||
description: '资产管理菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/assets/',
|
||||
menu_icon: 'Social',
|
||||
type: 1,
|
||||
sort_key: 40,
|
||||
created_at: '2025-12-26T13:23:52.268069+08:00',
|
||||
},
|
||||
{
|
||||
id: 56,
|
||||
identity: '019b591d-036b-7cb0-8678-4e1a7c73442f',
|
||||
title: '设备分类管理',
|
||||
title_en: 'Device Category Management',
|
||||
code: 'ops:资产管理:设备分类管理',
|
||||
description: '资产管理 - 设备分类管理',
|
||||
app_id: 2,
|
||||
parent_id: 54,
|
||||
menu_path: '/assets/classify',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 41,
|
||||
created_at: '2025-12-26T13:23:52.299831+08:00',
|
||||
},
|
||||
{
|
||||
id: 57,
|
||||
identity: '019b591d-037b-7248-8227-2ae17b0210fb',
|
||||
title: '设备管理',
|
||||
title_en: 'Device Management',
|
||||
code: 'ops:资产管理:设备管理',
|
||||
description: '资产管理 - 设备管理',
|
||||
app_id: 2,
|
||||
parent_id: 54,
|
||||
menu_path: '/assets/device',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 42,
|
||||
created_at: '2025-12-26T13:23:52.315149+08:00',
|
||||
},
|
||||
{
|
||||
id: 55,
|
||||
identity: '019b591d-035b-766f-9acf-a8e8dbcc5882',
|
||||
title: '供应商管理',
|
||||
title_en: 'Supplier Management',
|
||||
code: 'ops:资产管理:供应商管理',
|
||||
description: '资产管理 - 供应商管理',
|
||||
app_id: 2,
|
||||
parent_id: 54,
|
||||
menu_path: '/assets/supplier',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 43,
|
||||
created_at: '2025-12-26T13:23:52.283421+08:00',
|
||||
},
|
||||
{
|
||||
id: 63,
|
||||
identity: '019b591d-03d1-74e6-9547-a4321b0c0328',
|
||||
title: '知识库管理',
|
||||
title_en: 'Knowledge Base Management',
|
||||
code: 'ops:知识库管理',
|
||||
description: '知识库管理菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/kb/',
|
||||
menu_icon: 'FilePdf',
|
||||
type: 1,
|
||||
sort_key: 44,
|
||||
created_at: '2025-12-26T13:23:52.40915+08:00',
|
||||
},
|
||||
{
|
||||
id: 65,
|
||||
identity: '019b591d-03f8-78a9-ba28-c41ff1873569',
|
||||
title: '知识管理',
|
||||
title_en: 'Knowledge Base',
|
||||
code: 'ops:知识库管理:知识库',
|
||||
description: '知识库管理 - 知识库',
|
||||
app_id: 2,
|
||||
parent_id: 63,
|
||||
menu_path: '/kb/items',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 45,
|
||||
created_at: '2025-12-26T13:23:52.440567+08:00',
|
||||
},
|
||||
{
|
||||
id: 64,
|
||||
identity: '019b591d-03e8-7d4b-b8cf-7a5142320c61',
|
||||
title: '标签管理',
|
||||
title_en: 'Tag Management',
|
||||
code: 'ops:知识库管理:标签管理',
|
||||
description: '知识库管理 - 标签管理',
|
||||
app_id: 2,
|
||||
parent_id: 63,
|
||||
menu_path: '/kb/tags',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 46,
|
||||
created_at: '2025-12-26T13:23:52.424871+08:00',
|
||||
},
|
||||
{
|
||||
id: 67,
|
||||
identity: '019b591d-0410-7c89-a545-98ca76421c41',
|
||||
title: '我的审核',
|
||||
title_en: 'Review',
|
||||
code: 'ops:知识库管理:审核',
|
||||
description: '知识库管理 - 审核',
|
||||
app_id: 2,
|
||||
parent_id: 63,
|
||||
menu_path: '/kb/review',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 47,
|
||||
created_at: '2025-12-26T13:23:52.464821+08:00',
|
||||
},
|
||||
{
|
||||
id: 68,
|
||||
identity: '019b591d-0427-7292-b964-ea747ac59748',
|
||||
title: '我的收藏',
|
||||
title_en: 'My Favorites',
|
||||
code: 'ops:知识库管理:我的收藏',
|
||||
description: '知识库管理 - 我的收藏',
|
||||
app_id: 2,
|
||||
parent_id: 63,
|
||||
menu_path: '/kb/favorite',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 48,
|
||||
created_at: '2025-12-26T13:23:52.487168+08:00',
|
||||
},
|
||||
{
|
||||
id: 66,
|
||||
identity: '019b591d-0408-748f-a43e-3fc6aaabe397',
|
||||
title: '回收站',
|
||||
title_en: 'Recycle Bin',
|
||||
code: 'ops:知识库管理:回收站',
|
||||
description: '知识库管理 - 回收站',
|
||||
app_id: 2,
|
||||
parent_id: 63,
|
||||
menu_path: '/kb/recycle',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 49,
|
||||
created_at: '2025-12-26T13:23:52.456298+08:00',
|
||||
},
|
||||
{
|
||||
id: 69,
|
||||
identity: '019b591d-042e-7d67-98c6-432768a6fd97',
|
||||
title: '报告管理',
|
||||
title_en: 'Report Management',
|
||||
code: 'ops:报告管理',
|
||||
description: '报表管理菜单',
|
||||
app_id: 2,
|
||||
menu_path: '/report/',
|
||||
menu_icon: 'Git',
|
||||
type: 1,
|
||||
sort_key: 50,
|
||||
created_at: '2025-12-26T13:23:52.502439+08:00',
|
||||
},
|
||||
{
|
||||
id: 73,
|
||||
identity: '019b591d-0475-7b37-929a-2b652b7de8cd',
|
||||
title: '服务器报告',
|
||||
title_en: 'Server Report',
|
||||
code: 'ops:报表管理:服务器报表',
|
||||
description: '报表管理 - 服务器报表',
|
||||
app_id: 2,
|
||||
parent_id: 69,
|
||||
menu_path: '/report/host',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 51,
|
||||
created_at: '2025-12-26T13:23:52.565735+08:00',
|
||||
},
|
||||
{
|
||||
id: 72,
|
||||
identity: '019b591d-0465-769b-8eca-a3ed252243d8',
|
||||
title: '故障报告',
|
||||
title_en: 'Fault Report',
|
||||
code: 'ops:报表管理:故障报告',
|
||||
description: '报表管理 - 故障报告',
|
||||
app_id: 2,
|
||||
parent_id: 69,
|
||||
menu_path: '/report/fault',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 52,
|
||||
created_at: '2025-12-26T13:23:52.549432+08:00',
|
||||
},
|
||||
{
|
||||
id: 74,
|
||||
identity: '019b591d-0484-7ca3-8535-5d87dd16305a',
|
||||
title: '网络设备报告',
|
||||
title_en: 'Network Device Report',
|
||||
code: 'ops:报表管理:网络设备报表',
|
||||
description: '报表管理 - 网络设备报表',
|
||||
app_id: 2,
|
||||
parent_id: 69,
|
||||
menu_path: '/report/device',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 53,
|
||||
created_at: '2025-12-26T13:23:52.580828+08:00',
|
||||
},
|
||||
{
|
||||
id: 71,
|
||||
identity: '019b591d-0455-7a93-9386-7825fa1e2aee',
|
||||
title: '流量统计报告',
|
||||
title_en: 'Traffic Statistics',
|
||||
code: 'ops:报表管理:流量统计',
|
||||
description: '报表管理 - 流量统计',
|
||||
app_id: 2,
|
||||
parent_id: 69,
|
||||
menu_path: '/report/traffic',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 54,
|
||||
created_at: '2025-12-26T13:23:52.533693+08:00',
|
||||
},
|
||||
{
|
||||
id: 75,
|
||||
identity: '019b591d-0495-7891-8893-5f93b073c4ba',
|
||||
title: '历史报告',
|
||||
title_en: 'History Report',
|
||||
code: 'ops:报表管理:历史报表',
|
||||
description: '报表管理 - 历史报表',
|
||||
app_id: 2,
|
||||
parent_id: 69,
|
||||
menu_path: '/report/history',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 55,
|
||||
created_at: '2025-12-26T13:23:52.597561+08:00',
|
||||
},
|
||||
{
|
||||
id: 70,
|
||||
identity: '019b591d-0446-7270-b3a6-b0a0e4623962',
|
||||
title: '统计报告',
|
||||
title_en: 'Statistics Report',
|
||||
code: 'ops:报表管理:统计报告',
|
||||
description: '报表管理 - 统计报告',
|
||||
app_id: 2,
|
||||
parent_id: 69,
|
||||
menu_path: '/report/statistics',
|
||||
menu_icon: 'appstore',
|
||||
type: 1,
|
||||
sort_key: 56,
|
||||
created_at: '2025-12-26T13:23:52.518159+08:00',
|
||||
},
|
||||
{
|
||||
id: 78,
|
||||
identity: '019b5dc5-d82f-7290-b805-f549db554120',
|
||||
title: '系统设置',
|
||||
title_en: 'System Settings',
|
||||
code: 'system-settings',
|
||||
app_id: 2,
|
||||
menu_path: '/system-settings/',
|
||||
menu_icon: 'Settings',
|
||||
type: 1,
|
||||
sort_key: 57,
|
||||
created_at: '2025-12-27T11:06:45.679492+08:00',
|
||||
},
|
||||
{
|
||||
id: 82,
|
||||
identity: '019b5e13-a5c0-7be8-88ff-7cc000325fbe',
|
||||
title: '系统监控',
|
||||
title_en: 'System Monitoring',
|
||||
code: 'SystemSettingsSystemMonitoring',
|
||||
description: '系统监控',
|
||||
app_id: 2,
|
||||
parent_id: 78,
|
||||
menu_path: '/system-settings/system-monitoring',
|
||||
menu_icon: 'icon-file',
|
||||
type: 1,
|
||||
sort_key: 58,
|
||||
created_at: '2025-12-27T12:31:44.577167+08:00',
|
||||
},
|
||||
{
|
||||
id: 81,
|
||||
identity: '019b5e13-019b-7e4c-9466-3173e25d8c57',
|
||||
title: '系统日志',
|
||||
title_en: 'System Logs',
|
||||
code: 'SystemSettingsSystemLogs',
|
||||
description: '系统日志',
|
||||
app_id: 2,
|
||||
parent_id: 78,
|
||||
menu_path: '/system-settings/system-logs',
|
||||
type: 1,
|
||||
sort_key: 59,
|
||||
created_at: '2025-12-27T12:31:02.556301+08:00',
|
||||
},
|
||||
{
|
||||
id: 80,
|
||||
identity: '019b5e12-8192-7d79-9dfc-b193e1ef53a0',
|
||||
title: '用户管理',
|
||||
title_en: 'Account Management',
|
||||
code: 'SystemSettingsAccountManagement',
|
||||
description: '用户管理',
|
||||
app_id: 2,
|
||||
parent_id: 78,
|
||||
menu_path: '/system-settings/account-management',
|
||||
type: 1,
|
||||
sort_key: 60,
|
||||
created_at: '2025-12-27T12:30:29.779261+08:00',
|
||||
},
|
||||
{
|
||||
id: 79,
|
||||
identity: '019b5e12-0315-7d3a-b85c-1a0a557d6853',
|
||||
title: '菜单设置',
|
||||
title_en: 'Menu Management',
|
||||
code: 'SystemSettingsMenuManagement',
|
||||
description: '菜单设置',
|
||||
app_id: 2,
|
||||
parent_id: 78,
|
||||
menu_path: '/system-settings/menu-management',
|
||||
type: 1,
|
||||
sort_key: 61,
|
||||
created_at: '2025-12-27T12:29:57.398258+08:00',
|
||||
},
|
||||
{
|
||||
id: 84,
|
||||
identity: '019be0c6-2c0c-7c15-b003-9b9b4384f422',
|
||||
title: '许可授权中心',
|
||||
title_en: 'LicenseCenter',
|
||||
code: 'LicenseCenter',
|
||||
app_id: 2,
|
||||
parent_id: 78,
|
||||
menu_path: '/system-settings/license-center',
|
||||
type: 1,
|
||||
sort_key: 62,
|
||||
created_at: '2026-01-21T21:37:22.445144+08:00',
|
||||
},
|
||||
{
|
||||
id: 86,
|
||||
identity: '019cc7bf-9bd4-78ed-bbcb-c5169236ca72',
|
||||
title: '帮助中心',
|
||||
title_en: 'Help Center',
|
||||
code: 'ops:帮助中心',
|
||||
app_id: 2,
|
||||
menu_path: '/help',
|
||||
menu_icon: 'icon-bulb',
|
||||
type: 1,
|
||||
sort_key: 63,
|
||||
created_at: '2026-03-07T18:02:29.205051+08:00',
|
||||
},
|
||||
]
|
||||
|
||||
export default localMenuFlatItems
|
||||
1006
src/router/local-menu-items.ts
Normal file
1006
src/router/local-menu-items.ts
Normal file
File diff suppressed because it is too large
Load Diff
736
src/router/menu-data.ts
Normal file
736
src/router/menu-data.ts
Normal file
@@ -0,0 +1,736 @@
|
||||
import { DEFAULT_LAYOUT } from './routes/base'
|
||||
import type { AppRouteRecordRaw } from './routes/types'
|
||||
|
||||
// 本地菜单数据 - 接口未准备好时使用
|
||||
export const localMenuData: AppRouteRecordRaw[] = [{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '仪表盘',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-dashboard',
|
||||
order: 0,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'workplace',
|
||||
name: 'Workplace',
|
||||
component: () => import('@/views/dashboard/workplace/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.dashboard.workplace',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
/** simple */
|
||||
{
|
||||
path: 'monitor',
|
||||
name: 'Monitor',
|
||||
component: () => import('@/views/dashboard/monitor/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.dashboard.monitor',
|
||||
requiresAuth: true,
|
||||
roles: ['admin'],
|
||||
},
|
||||
},
|
||||
/** simple end */
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/overview',
|
||||
name: 'Overview',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '系统概况',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-home',
|
||||
order: 1,
|
||||
hideChildrenInMenu: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'OverviewIndex',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '系统概况',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/visual',
|
||||
name: 'VisualDashboard',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '可视化大屏管理',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-apps',
|
||||
order: 2,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'component',
|
||||
name: 'VisualComponent',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '大屏管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'view-publish',
|
||||
name: 'ViewPublish',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '已发布大屏列表',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/dc',
|
||||
name: 'DataCenter',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '服务器及PC管理',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-storage',
|
||||
order: 3,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'pc',
|
||||
name: 'OfficePC',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '办公PC管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'server',
|
||||
name: 'ServerManagement',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '服务器管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/dc/cluster',
|
||||
name: 'ClusterCollection',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '集群采集控制中心',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-settings',
|
||||
order: 4,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'database',
|
||||
name: 'DatabaseCollection',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '数据库采集管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'middleware',
|
||||
name: 'MiddlewareCollection',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '中间件采集管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'network',
|
||||
name: 'NetworkDeviceCollection',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '网络设备采集管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/monitor',
|
||||
name: 'Monitor',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '综合监控',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-desktop',
|
||||
order: 5,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'log',
|
||||
name: 'LogMonitor',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '日志监控',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'virtualization',
|
||||
name: 'VirtualizationMonitor',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '虚拟化监控',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'storage',
|
||||
name: 'StorageMonitor',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '存储设备监控',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'network',
|
||||
name: 'NetworkMonitor',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '网络设备监控',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'power',
|
||||
name: 'PowerMonitor',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '电力/UPS/空调/温湿度',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'url',
|
||||
name: 'URLMonitor',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: 'URL监控',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'guard',
|
||||
name: 'GuardMonitor',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '消防/门禁/漏水/有害气体',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'security',
|
||||
name: 'SecurityMonitor',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '安全设备监控',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/netarch',
|
||||
name: 'NetworkArchitecture',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '网络架构管理',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-nav',
|
||||
order: 6,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'auto-topo',
|
||||
name: 'AutoTopo',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '自动感知拓扑图',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'topo-group',
|
||||
name: 'TopoGroup',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '拓扑管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'traffic',
|
||||
name: 'TrafficAnalysis',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '流量分析管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'ip',
|
||||
name: 'IPAddress',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: 'IP地址管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/alert',
|
||||
name: 'Alert',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '告警管理',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-bulb',
|
||||
order: 7,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'setting',
|
||||
name: 'AlertSetting',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '告警策略管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tackle',
|
||||
name: 'AlertTackle',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '告警受理处理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
name: 'AlertHistory',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '告警历史',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'template',
|
||||
name: 'AlertTemplate',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '告警模版',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'notice',
|
||||
name: 'AlertNotice',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '告警通知设置',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'level',
|
||||
name: 'AlertLevel',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '告警级别管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/feedback',
|
||||
name: 'Feedback',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '工单管理',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-list',
|
||||
order: 8,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'all',
|
||||
name: 'AllTickets',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '所有工单',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'undo',
|
||||
name: 'PendingTickets',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '我的工单',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/datacenter',
|
||||
name: 'DataCenterManagement',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '数据中心管理',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-drive-file',
|
||||
order: 9,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'rack',
|
||||
name: 'RackManagement',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '机柜管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'house',
|
||||
name: 'DataCenterHouse',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '数据中心',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'floor',
|
||||
name: 'FloorManagement',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '楼层管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/assets',
|
||||
name: 'Assets',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '资产管理',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-apps',
|
||||
order: 10,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'classify',
|
||||
name: 'AssetClassify',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '设备分类管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'device',
|
||||
name: 'AssetDevice',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '设备管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'supplier',
|
||||
name: 'AssetSupplier',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '供应商管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/kb',
|
||||
name: 'KnowledgeBase',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '知识库管理',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-file',
|
||||
order: 11,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'items',
|
||||
name: 'KnowledgeItems',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '知识管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tags',
|
||||
name: 'KnowledgeTags',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '标签管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'review',
|
||||
name: 'KnowledgeReview',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '我的审核',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'favorite',
|
||||
name: 'KnowledgeFavorite',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '我的收藏',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'recycle',
|
||||
name: 'KnowledgeRecycle',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '回收站',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/report',
|
||||
name: 'Report',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '报告管理',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-nav',
|
||||
order: 12,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'host',
|
||||
name: 'ServerReport',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '服务器报告',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'fault',
|
||||
name: 'FaultReport',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '故障报告',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'device',
|
||||
name: 'DeviceReport',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '网络设备报告',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'traffic',
|
||||
name: 'TrafficReport',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '流量统计报告',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'history',
|
||||
name: 'HistoryReport',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '历史报告',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'statistics',
|
||||
name: 'StatisticsReport',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '统计报告',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/system-settings',
|
||||
name: 'SystemSettings',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '系统设置',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-settings',
|
||||
order: 13,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'system-monitoring',
|
||||
name: 'SystemMonitoring',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '系统监控',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system-logs',
|
||||
name: 'SystemLogs',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '系统日志',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'account-management',
|
||||
name: 'AccountManagement',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '用户管理',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'menu-management',
|
||||
name: 'MenuManagement',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '菜单设置',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'license-center',
|
||||
name: 'LicenseCenter',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '许可授权中心',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/help',
|
||||
name: 'HelpCenter',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: '帮助中心',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-bulb',
|
||||
order: 14,
|
||||
hideChildrenInMenu: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'HelpCenterIndex',
|
||||
component: () => import('@/views/redirect/index.vue'),
|
||||
meta: {
|
||||
locale: '帮助中心',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default localMenuData
|
||||
@@ -6,7 +6,7 @@ const DASHBOARD: AppRouteRecordRaw = {
|
||||
name: 'dashboard',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: 'menu.dashboard',
|
||||
locale: '仪表盘',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-dashboard',
|
||||
order: 0,
|
||||
|
||||
@@ -13,21 +13,11 @@ const LIST: AppRouteRecordRaw = {
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'search-table', // The midline path complies with SEO specifications
|
||||
name: 'SearchTable',
|
||||
component: () => import('@/views/list/search-table/index.vue'),
|
||||
path: 'search-table-demo',
|
||||
name: 'SearchTableDemo',
|
||||
component: () => import('@/views/list/search-table/demo.vue'),
|
||||
meta: {
|
||||
locale: 'menu.list.searchTable',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'card',
|
||||
name: 'Card',
|
||||
component: () => import('@/views/list/card/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.list.cardList',
|
||||
locale: '公共组件Demo',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
|
||||
38
src/router/routes/modules/ops.ts
Normal file
38
src/router/routes/modules/ops.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { DEFAULT_LAYOUT } from '../base'
|
||||
import { AppRouteRecordRaw } from '../types'
|
||||
|
||||
const OPS: AppRouteRecordRaw = {
|
||||
path: '/ops',
|
||||
name: 'ops',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: 'menu.ops',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-settings',
|
||||
order: 3,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'menu-management',
|
||||
name: 'MenuManagement',
|
||||
component: () => import('@/views/ops/pages/system-settings/menu-management/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.ops.systemSettings.menuManagement',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system-logs',
|
||||
name: 'SystemLogs',
|
||||
component: () => import('@/views/ops/pages/system-settings/system-logs/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.ops.systemSettings.systemLogs',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default OPS
|
||||
@@ -1,38 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base'
|
||||
import { AppRouteRecordRaw } from '../types'
|
||||
|
||||
const USER: AppRouteRecordRaw = {
|
||||
path: '/user',
|
||||
name: 'user',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: 'menu.user',
|
||||
icon: 'icon-user',
|
||||
requiresAuth: true,
|
||||
order: 7,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'info',
|
||||
name: 'Info',
|
||||
component: () => import('@/views/user/info/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.user.info',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'setting',
|
||||
name: 'Setting',
|
||||
component: () => import('@/views/user/setting/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.user.setting',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default USER
|
||||
@@ -3,9 +3,13 @@ import { Notification } from '@arco-design/web-vue'
|
||||
import type { NotificationReturn } from '@arco-design/web-vue/es/notification/interface'
|
||||
import type { RouteRecordNormalized } from 'vue-router'
|
||||
import defaultSettings from '@/config/settings.json'
|
||||
import { getMenuList } from '@/api/user'
|
||||
import { userPmn } from '@/api/module/user'
|
||||
import { localMenuData } from '@/router/menu-data'
|
||||
import { buildTree } from '@/utils/tree'
|
||||
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage";
|
||||
import { AppState } from './types'
|
||||
|
||||
|
||||
const useAppStore = defineStore('app', {
|
||||
state: (): AppState => ({ ...defaultSettings }),
|
||||
|
||||
@@ -45,27 +49,24 @@ const useAppStore = defineStore('app', {
|
||||
this.hideMenu = value
|
||||
},
|
||||
async fetchServerMenuConfig() {
|
||||
const userInfo = SafeStorage.get(AppStorageKey.USER_INFO) as any
|
||||
let notifyInstance: NotificationReturn | null = null
|
||||
try {
|
||||
notifyInstance = Notification.info({
|
||||
id: 'menuNotice', // Keep the instance id the same
|
||||
content: 'loading',
|
||||
closable: true,
|
||||
})
|
||||
const { data } = await getMenuList()
|
||||
this.serverMenu = data
|
||||
notifyInstance = Notification.success({
|
||||
id: 'menuNotice',
|
||||
content: 'success',
|
||||
closable: true,
|
||||
})
|
||||
// 使用本地菜单数据(接口未准备好)
|
||||
// TODO: 接口准备好后,取消下面的注释,使用真实接口数据
|
||||
const res = await userPmn({ id: userInfo.user_id, workspace: import.meta.env.VITE_APP_WORKSPACE })
|
||||
console.log('res', res)
|
||||
if (res.code === 0 && res?.details?.length) {
|
||||
console.log('buildTree', buildTree(res.details[0].permissions))
|
||||
}
|
||||
// this.serverMenu = data
|
||||
|
||||
// 使用本地数据
|
||||
this.serverMenu = localMenuData as unknown as RouteRecordNormalized[]
|
||||
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
notifyInstance = Notification.error({
|
||||
id: 'menuNotice',
|
||||
content: 'error',
|
||||
closable: true,
|
||||
})
|
||||
|
||||
}
|
||||
},
|
||||
clearServerMenu() {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { LoginData, getUserInfo, login as userLogin, logout as userLogout } from '@/api/user'
|
||||
import { login, logout } from '@/api/module/user'
|
||||
import { LoginData } from '@/api/types'
|
||||
import { request } from '@/api/request'
|
||||
import { clearToken, setToken } from '@/utils/auth'
|
||||
import { removeRouteListener } from '@/utils/route-listener'
|
||||
import { defineStore } from 'pinia'
|
||||
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage";
|
||||
import useAppStore from '../app'
|
||||
import { UserState } from './types'
|
||||
|
||||
@@ -23,6 +26,7 @@ const useUserStore = defineStore('user', {
|
||||
accountId: undefined,
|
||||
certification: undefined,
|
||||
role: '',
|
||||
userInfo: SafeStorage.get(AppStorageKey.USER_INFO)
|
||||
}),
|
||||
|
||||
getters: {
|
||||
@@ -50,16 +54,22 @@ const useUserStore = defineStore('user', {
|
||||
|
||||
// Get user's information
|
||||
async info() {
|
||||
const res = await getUserInfo()
|
||||
|
||||
this.setInfo(res.data)
|
||||
// const res = await request.post('/rbac2/v1/user/info') as any
|
||||
// request 拦截器已经返回 response.data,所以 res 就是用户信息
|
||||
// this.setInfo(res.data || res)
|
||||
},
|
||||
|
||||
// Login
|
||||
async login(loginForm: LoginData) {
|
||||
try {
|
||||
const res = await userLogin(loginForm)
|
||||
setToken(res.data.token)
|
||||
const { code, details } = await login(loginForm as any) as any
|
||||
if (code === 0 && details?.token) {
|
||||
setToken(details.token)
|
||||
SafeStorage.set(AppStorageKey.USER_INFO, details)
|
||||
this.userInfo = details
|
||||
} else {
|
||||
throw new Error('登录失败:未获取到 token')
|
||||
}
|
||||
} catch (err) {
|
||||
clearToken()
|
||||
throw err
|
||||
@@ -71,11 +81,12 @@ const useUserStore = defineStore('user', {
|
||||
clearToken()
|
||||
removeRouteListener()
|
||||
appStore.clearServerMenu()
|
||||
SafeStorage.clearAppStorage()
|
||||
},
|
||||
// Logout
|
||||
async logout() {
|
||||
try {
|
||||
await userLogout()
|
||||
// await logout()
|
||||
} finally {
|
||||
this.logoutCallBack()
|
||||
}
|
||||
|
||||
@@ -16,4 +16,5 @@ export interface UserState {
|
||||
accountId?: string
|
||||
certification?: number
|
||||
role: RoleType
|
||||
userInfo?: any
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
const TOKEN_KEY = 'token'
|
||||
import SafeStorage, { AppStorageKey } from "@/utils/safeStorage";
|
||||
|
||||
const isLogin = () => {
|
||||
return !!localStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
const isLogin = () => !!SafeStorage.get(AppStorageKey.TOKEN)
|
||||
|
||||
const getToken = () => {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
const getToken = () => SafeStorage.get(AppStorageKey.TOKEN)
|
||||
|
||||
const setToken = (token: string) => {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
SafeStorage.set(AppStorageKey.TOKEN, token)
|
||||
}
|
||||
|
||||
const clearToken = () => {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
SafeStorage.remove(AppStorageKey.TOKEN)
|
||||
}
|
||||
|
||||
export { clearToken, getToken, isLogin, setToken }
|
||||
|
||||
158
src/utils/safeStorage.ts
Normal file
158
src/utils/safeStorage.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 类型安全的 localStorage 封装,具备严格的 key 管理
|
||||
*/
|
||||
|
||||
// 1. 定义应用程序使用的存储键名(防止使用任意字符串作为 key)
|
||||
export enum AppStorageKey {
|
||||
PASSPORT_DATA = 'passportData',
|
||||
PASSPORT_TOKEN = 'passportToken',
|
||||
MENU_DATA = 'menuData', // 全部的菜单数据
|
||||
SLIDER_MENU = 'sliderMenu', // 侧边栏菜单数据
|
||||
USER_INFO = 'userInfo',
|
||||
TOKEN = 'token',
|
||||
MODE = 'mode', // 日间夜间模式
|
||||
}
|
||||
|
||||
// 2. 存储值类型定义(用于内部处理)
|
||||
type StorageValue<T> = {
|
||||
__data: T; // 实际存储的数据
|
||||
__expiry?: number; // 可选的过期时间戳
|
||||
};
|
||||
|
||||
// 3. 主存储类
|
||||
class SafeStorage {
|
||||
/**
|
||||
* 存储数据(自动处理 JSON 序列化)
|
||||
* @param key 预定义的存储键
|
||||
* @param value 要存储的值(支持所有 JSON 安全类型)
|
||||
* @param ttl 可选的时间有效期(毫秒)
|
||||
*/
|
||||
static set<T>(key: AppStorageKey, value: T, ttl?: number): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
// 创建存储对象(包含数据值和过期时间)
|
||||
const storageValue: StorageValue<T> = {
|
||||
__data: value,
|
||||
};
|
||||
|
||||
// 设置过期时间(如果提供)
|
||||
if (ttl !== undefined && ttl > 0) {
|
||||
storageValue.__expiry = Date.now() + ttl;
|
||||
}
|
||||
|
||||
// 自动序列化并存储
|
||||
localStorage.setItem(key, JSON.stringify(storageValue));
|
||||
} catch (error) {
|
||||
console.error(`[存储] 保存失败 (key: ${key})`, error);
|
||||
this.handleStorageError(key, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据(自动反序列化并检查过期)
|
||||
* @param key 预定义的存储键
|
||||
* @returns 存储的值或 null(如果不存在或过期)
|
||||
*/
|
||||
static get<T>(key: AppStorageKey): T | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
|
||||
try {
|
||||
// 获取原始数据
|
||||
const rawData = localStorage.getItem(key);
|
||||
if (!rawData) return null;
|
||||
|
||||
// 解析为内部存储结构
|
||||
const storageValue = JSON.parse(rawData) as StorageValue<T>;
|
||||
|
||||
// 检查是否过期
|
||||
if (this.isExpired(storageValue)) {
|
||||
this.remove(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回实际数据
|
||||
return storageValue.__data;
|
||||
} catch (error) {
|
||||
console.error(`[存储] 解析失败 (key: ${key})`, error);
|
||||
this.remove(key); // 移除无效数据
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定存储项
|
||||
* @param key 要删除的存储键
|
||||
*/
|
||||
static remove(key: AppStorageKey): void {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除应用相关的所有存储
|
||||
*/
|
||||
static clearAppStorage(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
// 遍历所有预定义的 key 进行删除
|
||||
Object.values(AppStorageKey).forEach(key => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------- 私有辅助方法 -----------------------------
|
||||
|
||||
/**
|
||||
* 检查存储值是否过期
|
||||
*/
|
||||
private static isExpired<T>(value: StorageValue<T>): boolean {
|
||||
return value.__expiry !== undefined && Date.now() > value.__expiry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理存储错误(配额不足等)
|
||||
*/
|
||||
private static handleStorageError(key: AppStorageKey, error: unknown): void {
|
||||
// 处理存储空间不足错误
|
||||
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
|
||||
console.warn('存储空间不足,尝试清理过期数据');
|
||||
this.clearExpiredItems();
|
||||
|
||||
// 尝试重新存储(最多尝试一次)
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw) {
|
||||
const value = JSON.parse(raw) as StorageValue<unknown>;
|
||||
if (!this.isExpired(value)) {
|
||||
localStorage.setItem(key, raw);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (retryError) {
|
||||
console.error('重试存储失败', retryError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有过期的存储项
|
||||
*/
|
||||
private static clearExpiredItems(): void {
|
||||
Object.values(AppStorageKey).forEach(key => {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
if (raw) {
|
||||
const value = JSON.parse(raw) as StorageValue<unknown>;
|
||||
if (this.isExpired(value)) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略无效数据
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default SafeStorage;
|
||||
223
src/utils/tree.ts
Normal file
223
src/utils/tree.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 树结构相关工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 树节点基础接口
|
||||
*/
|
||||
export interface TreeNodeBase {
|
||||
id?: number | string
|
||||
parent_id?: number | string | null
|
||||
children?: TreeNodeBase[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建树结构的选项
|
||||
*/
|
||||
export interface BuildTreeOptions<T extends TreeNodeBase> {
|
||||
/** ID 字段名,默认 'id' */
|
||||
idKey?: keyof T
|
||||
/** 父ID 字段名,默认 'parent_id' */
|
||||
parentKey?: keyof T
|
||||
/** 子节点字段名,默认 'children' */
|
||||
childrenKey?: string
|
||||
/** 排序字段名,可选 */
|
||||
orderKey?: keyof T
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建树结构的结果
|
||||
*/
|
||||
export interface BuildTreeResult<T extends TreeNodeBase> {
|
||||
/** 根节点列表 */
|
||||
rootItems: T[]
|
||||
/** 节点映射表 (id -> node) */
|
||||
itemMap: Map<number | string, T>
|
||||
}
|
||||
|
||||
/**
|
||||
* 将扁平数组构建为树状结构
|
||||
* @param items 扁平数组
|
||||
* @param options 构建选项
|
||||
* @returns 包含根节点列表和节点映射的对象
|
||||
*/
|
||||
export function buildTree<T extends TreeNodeBase>(
|
||||
items: T[],
|
||||
options: BuildTreeOptions<T> = {}
|
||||
): BuildTreeResult<T> {
|
||||
const {
|
||||
idKey = 'id' as keyof T,
|
||||
parentKey = 'parent_id' as keyof T,
|
||||
childrenKey = 'children',
|
||||
orderKey,
|
||||
} = options
|
||||
|
||||
const itemMap = new Map<number | string, T>()
|
||||
const rootItems: T[] = []
|
||||
|
||||
// 创建节点映射
|
||||
items.forEach((item) => {
|
||||
const id = item[idKey]
|
||||
if (id !== undefined && id !== null) {
|
||||
// 创建带有空 children 数组的节点副本
|
||||
itemMap.set(id as number | string, {
|
||||
...item,
|
||||
[childrenKey]: []
|
||||
} as T)
|
||||
}
|
||||
})
|
||||
|
||||
// 构建树结构
|
||||
itemMap.forEach((item) => {
|
||||
const parentId = item[parentKey]
|
||||
if (parentId && itemMap.has(parentId as number | string)) {
|
||||
const parent = itemMap.get(parentId as number | string)!
|
||||
const parentChildren = parent[childrenKey] as T[]
|
||||
parentChildren.push(item)
|
||||
} else {
|
||||
rootItems.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
// 排序函数
|
||||
if (orderKey) {
|
||||
const sortByOrder = (a: T, b: T) => {
|
||||
const orderA = (a[orderKey] as number) || 0
|
||||
const orderB = (b[orderKey] as number) || 0
|
||||
return orderA - orderB
|
||||
}
|
||||
|
||||
rootItems.sort(sortByOrder)
|
||||
itemMap.forEach((item) => {
|
||||
const children = item[childrenKey] as T[]
|
||||
if (children && children.length > 0) {
|
||||
children.sort(sortByOrder)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { rootItems, itemMap }
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历树节点
|
||||
* @param nodes 节点列表
|
||||
* @param callback 回调函数
|
||||
* @param childrenKey 子节点字段名
|
||||
*/
|
||||
export function traverseTree<T extends TreeNodeBase>(
|
||||
nodes: T[],
|
||||
callback: (node: T, depth: number, parent: T | null) => void | boolean,
|
||||
childrenKey: string = 'children',
|
||||
depth: number = 0,
|
||||
parent: T | null = null
|
||||
): void {
|
||||
for (const node of nodes) {
|
||||
const result = callback(node, depth, parent)
|
||||
// 如果回调返回 false,停止遍历
|
||||
if (result === false) return
|
||||
|
||||
const children = node[childrenKey] as T[]
|
||||
if (children && children.length > 0) {
|
||||
traverseTree(children, callback, childrenKey, depth + 1, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在树中查找节点
|
||||
* @param nodes 节点列表
|
||||
* @param predicate 判断条件
|
||||
* @param childrenKey 子节点字段名
|
||||
* @returns 找到的节点或 null
|
||||
*/
|
||||
export function findInTree<T extends TreeNodeBase>(
|
||||
nodes: T[],
|
||||
predicate: (node: T) => boolean,
|
||||
childrenKey: string = 'children'
|
||||
): T | null {
|
||||
for (const node of nodes) {
|
||||
if (predicate(node)) {
|
||||
return node
|
||||
}
|
||||
const children = node[childrenKey] as T[]
|
||||
if (children && children.length > 0) {
|
||||
const found = findInTree(children, predicate, childrenKey)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的所有父节点路径
|
||||
* @param nodeId 目标节点ID
|
||||
* @param itemMap 节点映射表
|
||||
* @param parentKey 父ID字段名
|
||||
* @returns 从根到目标节点的路径(不包含目标节点本身)
|
||||
*/
|
||||
export function getAncestors<T extends TreeNodeBase>(
|
||||
nodeId: number | string,
|
||||
itemMap: Map<number | string, T>,
|
||||
parentKey: keyof T = 'parent_id' as keyof T
|
||||
): T[] {
|
||||
const ancestors: T[] = []
|
||||
let current = itemMap.get(nodeId)
|
||||
|
||||
while (current) {
|
||||
const parentId = current[parentKey]
|
||||
if (parentId && itemMap.has(parentId as number | string)) {
|
||||
const parent = itemMap.get(parentId as number | string)!
|
||||
ancestors.unshift(parent)
|
||||
current = parent
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ancestors
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的所有子孙节点
|
||||
* @param node 目标节点
|
||||
* @param childrenKey 子节点字段名
|
||||
* @returns 所有子孙节点(扁平数组)
|
||||
*/
|
||||
export function getDescendants<T extends TreeNodeBase>(
|
||||
node: T,
|
||||
childrenKey: string = 'children'
|
||||
): T[] {
|
||||
const descendants: T[] = []
|
||||
const children = node[childrenKey] as T[]
|
||||
|
||||
if (children && children.length > 0) {
|
||||
for (const child of children) {
|
||||
descendants.push(child)
|
||||
descendants.push(...getDescendants(child, childrenKey))
|
||||
}
|
||||
}
|
||||
|
||||
return descendants
|
||||
}
|
||||
|
||||
/**
|
||||
* 将树结构扁平化为数组
|
||||
* @param nodes 节点列表
|
||||
* @param childrenKey 子节点字段名
|
||||
* @returns 扁平化后的数组(移除 children 属性)
|
||||
*/
|
||||
export function flattenTree<T extends TreeNodeBase>(
|
||||
nodes: T[],
|
||||
childrenKey: string = 'children'
|
||||
): Omit<T, 'children'>[] {
|
||||
const result: Omit<T, 'children'>[] = []
|
||||
|
||||
traverseTree(nodes, (node) => {
|
||||
const { [childrenKey]: _, ...rest } = node
|
||||
result.push(rest as Omit<T, 'children'>)
|
||||
}, childrenKey)
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
<template>
|
||||
<div class="card-wrap">
|
||||
<a-card v-if="loading" :bordered="false" hoverable>
|
||||
<slot name="skeleton"></slot>
|
||||
</a-card>
|
||||
<a-card v-else :bordered="false" hoverable>
|
||||
<a-space align="start">
|
||||
<a-avatar v-if="icon" :size="24" style="margin-right: 8px; background-color: #626aea">
|
||||
<icon-filter />
|
||||
</a-avatar>
|
||||
<a-card-meta>
|
||||
<template #title>
|
||||
<a-typography-text style="margin-right: 10px">
|
||||
{{ title }}
|
||||
</a-typography-text>
|
||||
<template v-if="showTag">
|
||||
<a-tag v-if="open && isExpires === false" size="small" color="green">
|
||||
<template #icon>
|
||||
<icon-check-circle-fill />
|
||||
</template>
|
||||
<span>{{ tagText }}</span>
|
||||
</a-tag>
|
||||
<a-tag v-else-if="isExpires" size="small" color="red">
|
||||
<template #icon>
|
||||
<icon-check-circle-fill />
|
||||
</template>
|
||||
<span>{{ expiresTagText }}</span>
|
||||
</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template #description>
|
||||
{{ description }}
|
||||
<slot></slot>
|
||||
</template>
|
||||
</a-card-meta>
|
||||
</a-space>
|
||||
<template #actions>
|
||||
<a-switch v-if="actionType === 'switch'" v-model="open" />
|
||||
<a-space v-else-if="actionType === 'button'">
|
||||
<template v-if="isExpires">
|
||||
<a-button type="outline" @click="renew">
|
||||
{{ expiresText }}
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-button v-if="open" @click="handleToggle">
|
||||
{{ closeTxt }}
|
||||
</a-button>
|
||||
<a-button v-else-if="!open" type="outline" @click="handleToggle">
|
||||
{{ openTxt }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-space>
|
||||
<div v-else>
|
||||
<a-space>
|
||||
<a-button @click="toggle(false)">
|
||||
{{ closeTxt }}
|
||||
</a-button>
|
||||
<a-button type="primary" @click="toggle(true)">
|
||||
{{ openTxt }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { useToggle } from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
actionType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
defaultValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
openTxt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
closeTxt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
expiresText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showTag: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
tagText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
expires: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
expiresTagText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
const [open, toggle] = useToggle(props.defaultValue)
|
||||
const handleToggle = () => {
|
||||
toggle()
|
||||
}
|
||||
const isExpires = ref(props.expires)
|
||||
const renew = () => {
|
||||
isExpires.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.card-wrap {
|
||||
height: 100%;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid var(--color-neutral-3);
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
// box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
:deep(.arco-card) {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
.arco-card-body {
|
||||
height: 100%;
|
||||
.arco-space {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.arco-space-item {
|
||||
height: 100%;
|
||||
&:last-child {
|
||||
flex: 1;
|
||||
}
|
||||
.arco-card-meta {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
.arco-card-meta-content {
|
||||
flex: 1;
|
||||
.arco-card-meta-description {
|
||||
margin-top: 8px;
|
||||
color: rgb(var(--gray-6));
|
||||
line-height: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.arco-card-meta-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.arco-card-meta-title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
// To prevent the shaking
|
||||
line-height: 28px;
|
||||
}
|
||||
:deep(.arco-skeleton-line) {
|
||||
&:last-child {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<div class="list-wrap">
|
||||
<a-typography-title class="block-title" :heading="6">
|
||||
{{ $t('cardList.tab.title.content') }}
|
||||
</a-typography-title>
|
||||
<a-row class="list-row" :gutter="24">
|
||||
<a-col :xs="12" :sm="12" :md="12" :lg="6" :xl="6" :xxl="6" class="list-col">
|
||||
<div class="card-wrap empty-wrap">
|
||||
<a-card :bordered="false" hoverable>
|
||||
<a-result :status="null" :title="$t('cardList.content.action')">
|
||||
<template #icon>
|
||||
<icon-plus style="font-size: 20px" />
|
||||
</template>
|
||||
</a-result>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col v-for="item in renderData" :key="item.id" class="list-col" :xs="12" :sm="12" :md="12" :lg="6" :xl="6" :xxl="6">
|
||||
<CardWrap
|
||||
:loading="loading"
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:default-value="item.enable"
|
||||
:action-type="item.actionType"
|
||||
:icon="item.icon"
|
||||
:open-txt="$t('cardList.content.inspection')"
|
||||
:close-txt="$t('cardList.content.delete')"
|
||||
:show-tag="false"
|
||||
>
|
||||
<a-descriptions style="margin-top: 16px" :data="item.data" layout="inline-horizontal" :column="2" />
|
||||
<template #skeleton>
|
||||
<a-skeleton :animation="true">
|
||||
<a-skeleton-line :widths="['50%', '50%', '100%', '40%']" :rows="4" />
|
||||
<a-skeleton-line :widths="['40%']" :rows="1" />
|
||||
</a-skeleton>
|
||||
</template>
|
||||
</CardWrap>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { queryInspectionList, ServiceRecord } from '@/api/list'
|
||||
import useRequest from '@/hooks/request'
|
||||
import CardWrap from './card-wrap.vue'
|
||||
|
||||
const defaultValue: ServiceRecord[] = new Array(3).fill({})
|
||||
const { loading, response: renderData } = useRequest<ServiceRecord[]>(queryInspectionList, defaultValue)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.card-wrap {
|
||||
height: 100%;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid var(--color-neutral-3);
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
:deep(.arco-card-meta-description) {
|
||||
color: rgb(var(--gray-6));
|
||||
.arco-descriptions-item-label-inline {
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
.arco-descriptions-item-value-inline {
|
||||
color: rgb(var(--gray-8));
|
||||
}
|
||||
}
|
||||
}
|
||||
.empty-wrap {
|
||||
height: 200px;
|
||||
border-radius: 4px;
|
||||
:deep(.arco-card) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
.arco-result-title {
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<div class="list-wrap">
|
||||
<a-typography-title class="block-title" :heading="6">
|
||||
{{ $t('cardList.tab.title.preset') }}
|
||||
</a-typography-title>
|
||||
<a-row class="list-row" :gutter="24">
|
||||
<a-col
|
||||
v-for="item in renderData"
|
||||
:key="item.id"
|
||||
:xs="12"
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="6"
|
||||
:xl="6"
|
||||
:xxl="6"
|
||||
class="list-col"
|
||||
style="min-height: 140px"
|
||||
>
|
||||
<CardWrap
|
||||
:loading="loading"
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:default-value="item.enable"
|
||||
:action-type="item.actionType"
|
||||
:tag-text="$t('cardList.preset.tag')"
|
||||
>
|
||||
<template #skeleton>
|
||||
<a-skeleton :animation="true">
|
||||
<a-skeleton-line :widths="['100%', '40%']" :rows="2" />
|
||||
<a-skeleton-line :widths="['40%']" :rows="1" />
|
||||
</a-skeleton>
|
||||
</template>
|
||||
</CardWrap>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { queryRulesPresetList, ServiceRecord } from '@/api/list'
|
||||
import useRequest from '@/hooks/request'
|
||||
import CardWrap from './card-wrap.vue'
|
||||
|
||||
const defaultValue: ServiceRecord[] = new Array(6).fill({})
|
||||
const { loading, response: renderData } = useRequest<ServiceRecord[]>(queryRulesPresetList, defaultValue)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<div class="list-wrap">
|
||||
<a-typography-title class="block-title" :heading="6">
|
||||
{{ $t('cardList.tab.title.service') }}
|
||||
</a-typography-title>
|
||||
<a-row class="list-row" :gutter="24">
|
||||
<a-col
|
||||
v-for="item in renderData"
|
||||
:key="item.id"
|
||||
:xs="12"
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="6"
|
||||
:xl="6"
|
||||
:xxl="6"
|
||||
class="list-col"
|
||||
style="min-height: 162px"
|
||||
>
|
||||
<CardWrap
|
||||
:loading="loading"
|
||||
:title="item.title"
|
||||
:description="item.description"
|
||||
:default-value="item.enable"
|
||||
:action-type="item.actionType"
|
||||
:expires="item.expires"
|
||||
:open-txt="$t('cardList.service.open')"
|
||||
:close-txt="$t('cardList.service.cancel')"
|
||||
:expires-text="$t('cardList.service.renew')"
|
||||
:tag-text="$t('cardList.service.tag')"
|
||||
:expires-tag-text="$t('cardList.service.expiresTag')"
|
||||
:icon="item.icon"
|
||||
>
|
||||
<template #skeleton>
|
||||
<a-skeleton :animation="true">
|
||||
<a-skeleton-line :widths="['100%', '40%', '100%']" :rows="3" />
|
||||
<a-skeleton-line :widths="['40%']" :rows="1" />
|
||||
</a-skeleton>
|
||||
</template>
|
||||
</CardWrap>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { queryTheServiceList, ServiceRecord } from '@/api/list'
|
||||
import useRequest from '@/hooks/request'
|
||||
import CardWrap from './card-wrap.vue'
|
||||
|
||||
const defaultValue: ServiceRecord[] = new Array(4).fill({})
|
||||
const { loading, response: renderData } = useRequest<ServiceRecord[]>(queryTheServiceList, defaultValue)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -1,92 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.list', 'menu.list.cardList']" />
|
||||
<a-row :gutter="20" align="stretch">
|
||||
<a-col :span="24">
|
||||
<a-card class="general-card" :title="$t('menu.list.cardList')">
|
||||
<a-row justify="space-between">
|
||||
<a-col :span="24">
|
||||
<a-tabs :default-active-tab="1" type="rounded">
|
||||
<a-tab-pane key="1" :title="$t('cardList.tab.title.all')">
|
||||
<QualityInspection />
|
||||
<TheService />
|
||||
<RulesPreset />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" :title="$t('cardList.tab.title.content')">
|
||||
<QualityInspection />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" :title="$t('cardList.tab.title.service')">
|
||||
<TheService />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" :title="$t('cardList.tab.title.preset')">
|
||||
<RulesPreset />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-col>
|
||||
<a-input-search
|
||||
:placeholder="$t('cardList.searchInput.placeholder')"
|
||||
style="width: 240px; position: absolute; top: 60px; right: 20px"
|
||||
/>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import QualityInspection from './components/quality-inspection.vue'
|
||||
import TheService from './components/the-service.vue'
|
||||
import RulesPreset from './components/rules-preset.vue'
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Card',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
:deep(.arco-list-content) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:deep(.arco-card-meta-title) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
:deep(.arco-list-col) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:deep(.arco-list-item) {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
:deep(.block-title) {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
:deep(.list-wrap) {
|
||||
// min-height: 140px;
|
||||
.list-row {
|
||||
align-items: stretch;
|
||||
.list-col {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
:deep(.arco-space) {
|
||||
width: 100%;
|
||||
.arco-space-item {
|
||||
&:last-child {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +0,0 @@
|
||||
export default {
|
||||
'menu.list.cardList': 'Card List',
|
||||
'cardList.tab.title.all': 'All',
|
||||
'cardList.tab.title.content': 'Quality Inspection',
|
||||
'cardList.tab.title.service': 'The service',
|
||||
'cardList.tab.title.preset': 'Rules Preset',
|
||||
'cardList.searchInput.placeholder': 'Search',
|
||||
'cardList.enable': 'Enable',
|
||||
'cardList.disable': 'Disable',
|
||||
'cardList.content.delete': 'Delete',
|
||||
'cardList.content.inspection': 'Inspection',
|
||||
'cardList.content.action': 'Click Create Qc Content queue',
|
||||
'cardList.service.open': 'Open',
|
||||
'cardList.service.cancel': 'Cancel',
|
||||
'cardList.service.renew': 'Contract of service',
|
||||
'cardList.service.tag': 'Opened',
|
||||
'cardList.service.expiresTag': 'Expired',
|
||||
'cardList.preset.tag': 'Enable',
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export default {
|
||||
'menu.list.cardList': '卡片列表',
|
||||
'cardList.tab.title.all': '全部',
|
||||
'cardList.tab.title.content': '内容质检',
|
||||
'cardList.tab.title.service': '开通服务',
|
||||
'cardList.tab.title.preset': '规则预置',
|
||||
'cardList.searchInput.placeholder': '搜索',
|
||||
// 'cardList.statistic.enable': '已启用',
|
||||
// 'cardList.statistic.disable': '未启用',
|
||||
'cardList.content.delete': '删除',
|
||||
'cardList.content.inspection': '质检',
|
||||
'cardList.content.action': '点击创建质检内容队列',
|
||||
'cardList.service.open': '开通服务',
|
||||
'cardList.service.cancel': '取消服务',
|
||||
'cardList.service.renew': '续约服务',
|
||||
'cardList.service.tag': '已开通',
|
||||
'cardList.service.expiresTag': '已过期',
|
||||
'cardList.preset.tag': '已启用',
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import Mock from 'mockjs'
|
||||
import setupMock, { successResponseWrap } from '@/utils/setup-mock'
|
||||
import { ServiceRecord } from '@/api/list'
|
||||
|
||||
const qualityInspectionList: ServiceRecord[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'quality',
|
||||
title: '视频类-历史导入',
|
||||
description: '2021-10-12 00:00:00',
|
||||
data: [
|
||||
{
|
||||
label: '待质检数',
|
||||
value: '120',
|
||||
},
|
||||
{
|
||||
label: '积压时长',
|
||||
value: '60s',
|
||||
},
|
||||
{
|
||||
label: '待抽检数',
|
||||
value: '0',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'quality',
|
||||
title: '图文类-图片版权',
|
||||
description: '2021-12-11 18:30:00',
|
||||
data: [
|
||||
{
|
||||
label: '待质检数',
|
||||
value: '120',
|
||||
},
|
||||
{
|
||||
label: '积压时长',
|
||||
value: '60s',
|
||||
},
|
||||
{
|
||||
label: '待抽检数',
|
||||
value: '0',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'quality',
|
||||
title: '图文类-高清图片',
|
||||
description: '2021-10-15 08:10:00',
|
||||
data: [
|
||||
{
|
||||
label: '待质检数',
|
||||
value: '120',
|
||||
},
|
||||
{
|
||||
label: '积压时长',
|
||||
value: '60s',
|
||||
},
|
||||
{
|
||||
label: '待抽检数',
|
||||
value: '0',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const theServiceList: ServiceRecord[] = [
|
||||
{
|
||||
id: 1,
|
||||
icon: 'code',
|
||||
title: '漏斗分析',
|
||||
description: '用户行为分析之漏斗分析模型是企业实现精细化运营、进行用户行为分析的重要数据分析模型。',
|
||||
enable: true,
|
||||
actionType: 'button',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: 'edit',
|
||||
title: '用户分布',
|
||||
description: '快速诊断用户人群,地域细分情况,了解数据分布的集中度,以及主要的数据分布的区间段是什么。',
|
||||
enable: true,
|
||||
actionType: 'button',
|
||||
expires: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: 'user',
|
||||
title: '资源分发',
|
||||
description: '移动端动态化资源分发解决方案。提供稳定大流量服务支持、灵活定制的分发圈选规则,通过离线化预加载。',
|
||||
enable: false,
|
||||
actionType: 'button',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: 'user',
|
||||
title: '用户画像分析',
|
||||
description: '用户画像就是将典型用户信息标签化,根据用户特征、业务场景和用户行为等信息,构建一个标签化的用户模型。',
|
||||
enable: true,
|
||||
actionType: 'button',
|
||||
},
|
||||
]
|
||||
const rulesPresetList: ServiceRecord[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: '内容屏蔽规则',
|
||||
description: '用户在执行特定的内容分发任务时,可使用内容屏蔽规则根据特定标签,过滤内容集合。',
|
||||
enable: true,
|
||||
actionType: 'switch',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '内容置顶规则',
|
||||
description: '该规则支持用户在执行特定内容分发任务时,对固定的几条内容置顶。',
|
||||
enable: true,
|
||||
actionType: 'switch',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '内容加权规则',
|
||||
description: '选定内容加权规则后可自定义从不同内容集合获取内容的概率。',
|
||||
enable: false,
|
||||
actionType: 'switch',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '内容分发规则',
|
||||
description: '内容分发时,对某些内容需要固定在C端展示的位置。',
|
||||
enable: true,
|
||||
actionType: 'switch',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '违禁内容识别',
|
||||
description: '精准识别赌博、刀枪、毒品、造假、贩假等违规物品和违规行为。',
|
||||
enable: false,
|
||||
actionType: 'switch',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '多语言文字符号识别',
|
||||
description: '精准识别英语、维语、藏语、蒙古语、朝鲜语等多种语言以及emoji表情形态的语义识别。',
|
||||
enable: false,
|
||||
actionType: 'switch',
|
||||
},
|
||||
]
|
||||
|
||||
setupMock({
|
||||
setup() {
|
||||
// Quality Inspection
|
||||
Mock.mock(new RegExp('/api/list/quality-inspection'), () => {
|
||||
return successResponseWrap(
|
||||
qualityInspectionList.map((_, index) => ({
|
||||
...qualityInspectionList[index % qualityInspectionList.length],
|
||||
id: Mock.Random.guid(),
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
// the service
|
||||
Mock.mock(new RegExp('/api/list/the-service'), () => {
|
||||
return successResponseWrap(
|
||||
theServiceList.map((_, index) => ({
|
||||
...theServiceList[index % theServiceList.length],
|
||||
id: Mock.Random.guid(),
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
// rules preset
|
||||
Mock.mock(new RegExp('/api/list/rules-preset'), () => {
|
||||
return successResponseWrap(
|
||||
rulesPresetList.map((_, index) => ({
|
||||
...rulesPresetList[index % rulesPresetList.length],
|
||||
id: Mock.Random.guid(),
|
||||
}))
|
||||
)
|
||||
})
|
||||
},
|
||||
})
|
||||
326
src/views/list/search-table/demo.vue
Normal file
326
src/views/list/search-table/demo.vue
Normal file
@@ -0,0 +1,326 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.list', '公共组件Demo']" />
|
||||
|
||||
<!-- 使用 SearchTable 公共组件 -->
|
||||
<SearchTable
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="公共组件演示"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
download-button-text="导出"
|
||||
refresh-tooltip-text="刷新数据"
|
||||
density-tooltip-text="表格密度"
|
||||
column-setting-tooltip-text="列设置"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
@download="handleDownload"
|
||||
>
|
||||
<!-- 工具栏左侧按钮 -->
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新增
|
||||
</a-button>
|
||||
<a-button status="success" @click="handleBatchDelete">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
批量删除
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- 表格自定义列:序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 表格自定义列:状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
|
||||
{{ record.status === 'active' ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 表格自定义列:头像 -->
|
||||
<template #avatar="{ record }">
|
||||
<a-avatar :style="{ backgroundColor: record.avatarColor }">
|
||||
{{ record.name.charAt(0) }}
|
||||
</a-avatar>
|
||||
</template>
|
||||
|
||||
<!-- 表格自定义列:操作 -->
|
||||
<template #operations="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text" size="small" @click="handleView(record)">
|
||||
查看
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm content="确定要删除吗?" @ok="handleDelete(record)">
|
||||
<a-button type="text" size="small" status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</SearchTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, reactive } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
// 定义表格数据类型
|
||||
interface UserRecord {
|
||||
id: number
|
||||
name: string
|
||||
email: string
|
||||
department: string
|
||||
role: string
|
||||
status: 'active' | 'inactive'
|
||||
avatarColor: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 模拟数据生成
|
||||
const generateMockData = (count: number): UserRecord[] => {
|
||||
const departments = ['技术部', '产品部', '运营部', '市场部', '财务部']
|
||||
const roles = ['管理员', '普通用户', '访客']
|
||||
const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十']
|
||||
const colors = ['#165DFF', '#0FC6C2', '#722ED1', '#F53F3F', '#FF7D00', '#00B42A']
|
||||
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: names[i % names.length] + (Math.floor(i / names.length) || ''),
|
||||
email: `user${i + 1}@example.com`,
|
||||
department: departments[i % departments.length],
|
||||
role: roles[i % roles.length],
|
||||
status: i % 3 === 0 ? 'inactive' : 'active',
|
||||
avatarColor: colors[i % colors.length],
|
||||
createdAt: `2024-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}`,
|
||||
}))
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<UserRecord[]>([])
|
||||
const formModel = ref({
|
||||
name: '',
|
||||
department: '',
|
||||
status: '',
|
||||
email: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'name',
|
||||
label: '用户名',
|
||||
type: 'input',
|
||||
placeholder: '请输入用户名',
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
label: '邮箱',
|
||||
type: 'input',
|
||||
placeholder: '请输入邮箱',
|
||||
},
|
||||
{
|
||||
field: 'department',
|
||||
label: '部门',
|
||||
type: 'select',
|
||||
placeholder: '请选择部门',
|
||||
options: [
|
||||
{ label: '技术部', value: '技术部' },
|
||||
{ label: '产品部', value: '产品部' },
|
||||
{ label: '运营部', value: '运营部' },
|
||||
{ label: '市场部', value: '市场部' },
|
||||
{ label: '财务部', value: '财务部' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
options: [
|
||||
{ label: '启用', value: 'active' },
|
||||
{ label: '禁用', value: 'inactive' },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '头像',
|
||||
dataIndex: 'avatar',
|
||||
slotName: 'avatar',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '邮箱',
|
||||
dataIndex: 'email',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '部门',
|
||||
dataIndex: 'department',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operations',
|
||||
slotName: 'operations',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
},
|
||||
])
|
||||
|
||||
// 模拟异步获取数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
let data = generateMockData(86)
|
||||
|
||||
// 根据搜索条件过滤
|
||||
if (formModel.value.name) {
|
||||
data = data.filter(item => item.name.includes(formModel.value.name))
|
||||
}
|
||||
if (formModel.value.email) {
|
||||
data = data.filter(item => item.email.includes(formModel.value.email))
|
||||
}
|
||||
if (formModel.value.department) {
|
||||
data = data.filter(item => item.department === formModel.value.department)
|
||||
}
|
||||
if (formModel.value.status) {
|
||||
data = data.filter(item => item.status === formModel.value.status)
|
||||
}
|
||||
|
||||
// 更新分页
|
||||
pagination.total = data.length
|
||||
|
||||
// 分页截取
|
||||
const start = (pagination.current - 1) * pagination.pageSize
|
||||
const end = start + pagination.pageSize
|
||||
tableData.value = data.slice(start, end)
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
name: '',
|
||||
department: '',
|
||||
status: '',
|
||||
email: '',
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
Message.info('导出功能开发中...')
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
Message.info('新增功能开发中...')
|
||||
}
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
Message.warning('请先选择要删除的数据')
|
||||
}
|
||||
|
||||
const handleView = (record: UserRecord) => {
|
||||
Message.info(`查看用户:${record.name}`)
|
||||
}
|
||||
|
||||
const handleEdit = (record: UserRecord) => {
|
||||
Message.info(`编辑用户:${record.name}`)
|
||||
}
|
||||
|
||||
const handleDelete = (record: UserRecord) => {
|
||||
Message.success(`已删除用户:${record.name}`)
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
fetchData()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SearchTableDemo',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,74 +1,27 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.list', 'menu.list.searchTable']" />
|
||||
<a-card class="general-card" :title="$t('menu.list.searchTable')">
|
||||
<a-row>
|
||||
<a-col :flex="1">
|
||||
<a-form :model="formModel" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 18 }" label-align="left">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item field="number" :label="$t('searchTable.form.number')">
|
||||
<a-input v-model="formModel.number" :placeholder="$t('searchTable.form.number.placeholder')" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="name" :label="$t('searchTable.form.name')">
|
||||
<a-input v-model="formModel.name" :placeholder="$t('searchTable.form.name.placeholder')" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="contentType" :label="$t('searchTable.form.contentType')">
|
||||
<a-select
|
||||
v-model="formModel.contentType"
|
||||
:options="contentTypeOptions"
|
||||
:placeholder="$t('searchTable.form.selectDefault')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="filterType" :label="$t('searchTable.form.filterType')">
|
||||
<a-select
|
||||
v-model="formModel.filterType"
|
||||
:options="filterTypeOptions"
|
||||
:placeholder="$t('searchTable.form.selectDefault')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="createdTime" :label="$t('searchTable.form.createdTime')">
|
||||
<a-range-picker v-model="formModel.createdTime" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="status" :label="$t('searchTable.form.status')">
|
||||
<a-select v-model="formModel.status" :options="statusOptions" :placeholder="$t('searchTable.form.selectDefault')" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-col>
|
||||
<a-divider style="height: 84px" direction="vertical" />
|
||||
<a-col :flex="'86px'" style="text-align: right">
|
||||
<a-space direction="vertical" :size="18">
|
||||
<a-button type="primary" @click="search">
|
||||
<template #icon>
|
||||
<icon-search />
|
||||
</template>
|
||||
{{ $t('searchTable.form.search') }}
|
||||
</a-button>
|
||||
<a-button @click="reset">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
{{ $t('searchTable.form.reset') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-divider style="margin-top: 0" />
|
||||
<a-row style="margin-bottom: 16px">
|
||||
<a-col :span="12">
|
||||
<a-space>
|
||||
<SearchTable
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="renderData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:title="$t('menu.list.searchTable')"
|
||||
:search-button-text="$t('searchTable.form.search')"
|
||||
:reset-button-text="$t('searchTable.form.reset')"
|
||||
:download-button-text="$t('searchTable.operation.download')"
|
||||
:refresh-tooltip-text="$t('searchTable.actions.refresh')"
|
||||
:density-tooltip-text="$t('searchTable.actions.density')"
|
||||
:column-setting-tooltip-text="$t('searchTable.actions.columnSetting')"
|
||||
@search="search"
|
||||
@reset="reset"
|
||||
@page-change="onPageChange"
|
||||
@refresh="search"
|
||||
>
|
||||
<!-- 工具栏左侧插槽 -->
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
@@ -82,60 +35,9 @@
|
||||
</a-button>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :span="12" style="display: flex; align-items: center; justify-content: end">
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<icon-download />
|
||||
</template>
|
||||
{{ $t('searchTable.operation.download') }}
|
||||
</a-button>
|
||||
<a-tooltip :content="$t('searchTable.actions.refresh')">
|
||||
<div class="action-icon" @click="search"><icon-refresh size="18" /></div>
|
||||
</a-tooltip>
|
||||
<a-dropdown @select="handleSelectDensity">
|
||||
<a-tooltip :content="$t('searchTable.actions.density')">
|
||||
<div class="action-icon"><icon-line-height size="18" /></div>
|
||||
</a-tooltip>
|
||||
<template #content>
|
||||
<a-doption v-for="item in densityList" :key="item.value" :value="item.value" :class="{ active: item.value === size }">
|
||||
<span>{{ item.name }}</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-tooltip :content="$t('searchTable.actions.columnSetting')">
|
||||
<a-popover trigger="click" position="bl" @popup-visible-change="popupVisibleChange">
|
||||
<div class="action-icon"><icon-settings size="18" /></div>
|
||||
<template #content>
|
||||
<div id="tableSetting">
|
||||
<div v-for="(item, index) in showColumns" :key="item.dataIndex" class="setting">
|
||||
<div style="margin-right: 4px; cursor: move">
|
||||
<icon-drag-arrow />
|
||||
</div>
|
||||
<div>
|
||||
<a-checkbox v-model="item.checked" @change="handleChange($event, item as TableColumnData, index)"></a-checkbox>
|
||||
</div>
|
||||
<div class="title">
|
||||
{{ item.title === '#' ? '序列号' : item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-table
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:columns="cloneColumns as TableColumnData[]"
|
||||
:data="renderData"
|
||||
:bordered="false"
|
||||
:size="size"
|
||||
@page-change="onPageChange"
|
||||
>
|
||||
|
||||
<!-- 表格自定义列插槽 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
@@ -175,24 +77,19 @@
|
||||
{{ $t('searchTable.columns.operations.view') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</SearchTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, reactive, watch, nextTick } from 'vue'
|
||||
import { computed, ref, reactive } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import { queryPolicyList, PolicyRecord, PolicyParams } from '@/api/list'
|
||||
import { Pagination } from '@/types/global'
|
||||
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
type SizeProps = 'mini' | 'small' | 'medium' | 'large'
|
||||
type Column = TableColumnData & { checked?: true }
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
const generateFormModel = () => {
|
||||
return {
|
||||
@@ -208,10 +105,6 @@ const { loading, setLoading } = useLoading(true)
|
||||
const { t } = useI18n()
|
||||
const renderData = ref<PolicyRecord[]>([])
|
||||
const formModel = ref(generateFormModel())
|
||||
const cloneColumns = ref<Column[]>([])
|
||||
const showColumns = ref<Column[]>([])
|
||||
|
||||
const size = ref<SizeProps>('medium')
|
||||
|
||||
const basePagination: Pagination = {
|
||||
current: 1,
|
||||
@@ -220,66 +113,7 @@ const basePagination: Pagination = {
|
||||
const pagination = reactive({
|
||||
...basePagination,
|
||||
})
|
||||
const densityList = computed(() => [
|
||||
{
|
||||
name: t('searchTable.size.mini'),
|
||||
value: 'mini',
|
||||
},
|
||||
{
|
||||
name: t('searchTable.size.small'),
|
||||
value: 'small',
|
||||
},
|
||||
{
|
||||
name: t('searchTable.size.medium'),
|
||||
value: 'medium',
|
||||
},
|
||||
{
|
||||
name: t('searchTable.size.large'),
|
||||
value: 'large',
|
||||
},
|
||||
])
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{
|
||||
title: t('searchTable.columns.index'),
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.number'),
|
||||
dataIndex: 'number',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.name'),
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.contentType'),
|
||||
dataIndex: 'contentType',
|
||||
slotName: 'contentType',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.filterType'),
|
||||
dataIndex: 'filterType',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.count'),
|
||||
dataIndex: 'count',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.createdTime'),
|
||||
dataIndex: 'createdTime',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.status'),
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.operations'),
|
||||
dataIndex: 'operations',
|
||||
slotName: 'operations',
|
||||
},
|
||||
])
|
||||
|
||||
const contentTypeOptions = computed<SelectOptionData[]>(() => [
|
||||
{
|
||||
label: t('searchTable.form.contentType.img'),
|
||||
@@ -314,6 +148,91 @@ const statusOptions = computed<SelectOptionData[]>(() => [
|
||||
value: 'offline',
|
||||
},
|
||||
])
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'number',
|
||||
label: t('searchTable.form.number'),
|
||||
type: 'input',
|
||||
placeholder: t('searchTable.form.number.placeholder'),
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
label: t('searchTable.form.name'),
|
||||
type: 'input',
|
||||
placeholder: t('searchTable.form.name.placeholder'),
|
||||
},
|
||||
{
|
||||
field: 'contentType',
|
||||
label: t('searchTable.form.contentType'),
|
||||
type: 'select',
|
||||
options: contentTypeOptions.value,
|
||||
},
|
||||
{
|
||||
field: 'filterType',
|
||||
label: t('searchTable.form.filterType'),
|
||||
type: 'select',
|
||||
options: filterTypeOptions.value,
|
||||
},
|
||||
{
|
||||
field: 'createdTime',
|
||||
label: t('searchTable.form.createdTime'),
|
||||
type: 'dateRange',
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: t('searchTable.form.status'),
|
||||
type: 'select',
|
||||
options: statusOptions.value,
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{
|
||||
title: t('searchTable.columns.index'),
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.number'),
|
||||
dataIndex: 'number',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.name'),
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.contentType'),
|
||||
dataIndex: 'contentType',
|
||||
slotName: 'contentType',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.filterType'),
|
||||
dataIndex: 'filterType',
|
||||
slotName: 'filterType',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.count'),
|
||||
dataIndex: 'count',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.createdTime'),
|
||||
dataIndex: 'createdTime',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.status'),
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
},
|
||||
{
|
||||
title: t('searchTable.columns.operations'),
|
||||
dataIndex: 'operations',
|
||||
slotName: 'operations',
|
||||
},
|
||||
])
|
||||
|
||||
const fetchData = async (params: PolicyParams = { current: 1, pageSize: 20 }) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -342,54 +261,6 @@ fetchData()
|
||||
const reset = () => {
|
||||
formModel.value = generateFormModel()
|
||||
}
|
||||
|
||||
const handleSelectDensity = (val: string | number | Record<string, any> | undefined, e: Event) => {
|
||||
size.value = val as SizeProps
|
||||
}
|
||||
|
||||
const handleChange = (checked: boolean | (string | boolean | number)[], column: Column, index: number) => {
|
||||
if (!checked) {
|
||||
cloneColumns.value = showColumns.value.filter((item) => item.dataIndex !== column.dataIndex)
|
||||
} else {
|
||||
cloneColumns.value.splice(index, 0, column)
|
||||
}
|
||||
}
|
||||
|
||||
const exchangeArray = <T extends Array<any>>(array: T, beforeIdx: number, newIdx: number, isDeep = false): T => {
|
||||
const newArray = isDeep ? cloneDeep(array) : array
|
||||
if (beforeIdx > -1 && newIdx > -1) {
|
||||
// 先替换后面的,然后拿到替换的结果替换前面的
|
||||
newArray.splice(beforeIdx, 1, newArray.splice(newIdx, 1, newArray[beforeIdx]).pop())
|
||||
}
|
||||
return newArray
|
||||
}
|
||||
|
||||
const popupVisibleChange = (val: boolean) => {
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
const el = document.getElementById('tableSetting') as HTMLElement
|
||||
const sortable = new Sortable(el, {
|
||||
onEnd(e: any) {
|
||||
const { oldIndex, newIndex } = e
|
||||
exchangeArray(cloneColumns.value, oldIndex, newIndex)
|
||||
exchangeArray(showColumns.value, oldIndex, newIndex)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => columns.value,
|
||||
(val) => {
|
||||
cloneColumns.value = cloneDeep(val)
|
||||
cloneColumns.value.forEach((item, index) => {
|
||||
item.checked = true
|
||||
})
|
||||
showColumns.value = cloneDeep(cloneColumns.value)
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -402,28 +273,4 @@ export default {
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
:deep(.arco-table-th) {
|
||||
&:last-child {
|
||||
.arco-table-th-item-title {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action-icon {
|
||||
margin-left: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.active {
|
||||
color: #0960bd;
|
||||
background-color: #e3f4fc;
|
||||
}
|
||||
.setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 200px;
|
||||
.title {
|
||||
margin-left: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
<div class="login-form-error-msg">{{ errorMessage }}</div>
|
||||
<a-form ref="loginForm" :model="userInfo" class="login-form" layout="vertical" @submit="handleSubmit">
|
||||
<a-form-item
|
||||
field="username"
|
||||
:rules="[{ required: true, message: $t('login.form.userName.errMsg') }]"
|
||||
field="account"
|
||||
:rules="[{ required: true, message: '请输入账号' }]"
|
||||
:validate-trigger="['change', 'blur']"
|
||||
hide-label
|
||||
>
|
||||
<a-input v-model="userInfo.username" :placeholder="$t('login.form.userName.placeholder')">
|
||||
<a-input v-model="userInfo.account" placeholder="账号">
|
||||
<template #prefix>
|
||||
<icon-user />
|
||||
</template>
|
||||
@@ -18,11 +18,11 @@
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="password"
|
||||
:rules="[{ required: true, message: $t('login.form.password.errMsg') }]"
|
||||
:rules="[{ required: true, message: '请输入密码' }]"
|
||||
:validate-trigger="['change', 'blur']"
|
||||
hide-label
|
||||
>
|
||||
<a-input-password v-model="userInfo.password" :placeholder="$t('login.form.password.placeholder')" allow-clear>
|
||||
<a-input-password v-model="userInfo.password" placeholder="密码" allow-clear>
|
||||
<template #prefix>
|
||||
<icon-lock />
|
||||
</template>
|
||||
@@ -44,7 +44,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { LoginData } from '@/api/user'
|
||||
import type { LoginData } from '@/api/types'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import { useUserStore } from '@/store'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
@@ -62,11 +62,11 @@ const userStore = useUserStore()
|
||||
|
||||
const loginConfig = useStorage('login-config', {
|
||||
rememberPassword: true,
|
||||
username: 'admin', // 演示默认值
|
||||
password: 'admin', // demo default value
|
||||
account: '',
|
||||
password: '',
|
||||
})
|
||||
const userInfo = reactive({
|
||||
username: loginConfig.value.username,
|
||||
account: loginConfig.value.account,
|
||||
password: loginConfig.value.password,
|
||||
})
|
||||
|
||||
@@ -85,10 +85,10 @@ const handleSubmit = async ({ errors, values }: { errors: Record<string, Validat
|
||||
})
|
||||
Message.success(t('login.form.login.success'))
|
||||
const { rememberPassword } = loginConfig.value
|
||||
const { username, password } = values
|
||||
const { account, password } = values
|
||||
// 实际生产环境需要进行加密存储。
|
||||
// The actual production environment requires encrypted storage.
|
||||
loginConfig.value.username = rememberPassword ? username : ''
|
||||
loginConfig.value.account = rememberPassword ? account : ''
|
||||
loginConfig.value.password = rememberPassword ? password : ''
|
||||
} catch (err) {
|
||||
errorMessage.value = (err as Error).message
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
<template>
|
||||
<div class="login-bg">
|
||||
<div class="container">
|
||||
<div class="scan-login-btn" @click="showQr = true">
|
||||
<svg class="scan-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||
<rect x="3" y="3" width="7" height="7" rx="2" fill="none" stroke="#00308f" stroke-width="2" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="2" fill="none" stroke="#00308f" stroke-width="2" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="2" fill="none" stroke="#00308f" stroke-width="2" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="2" fill="none" stroke="#00308f" stroke-width="2" />
|
||||
</svg>
|
||||
<span>扫码登录</span>
|
||||
</div>
|
||||
<a-modal v-model:visible="showQr" title="扫码登录" :footer="false" width="320px">
|
||||
<div class="qr-modal-content">
|
||||
<img
|
||||
src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=login-demo"
|
||||
alt="二维码"
|
||||
style="width: 200px; height: 200px; display: block; margin: 0 auto"
|
||||
/>
|
||||
<div style="text-align: center; margin-top: 12px; color: #888">请使用微信/钉钉等扫码登录</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
<div class="logo">
|
||||
<img alt="logo" src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image" />
|
||||
<div class="logo-text">智能运维管理系统</div>
|
||||
@@ -128,32 +109,4 @@ export default defineComponent({
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
.scan-login-btn {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 32px;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #f4f8ff;
|
||||
color: #00308f;
|
||||
border-radius: 18px;
|
||||
padding: 6px 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px 0 rgba(31, 38, 135, 0.08);
|
||||
transition: background 0.2s;
|
||||
&:hover {
|
||||
background: #e0e7ff;
|
||||
}
|
||||
.scan-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.qr-modal-content {
|
||||
padding: 12px 0 0 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,8 +4,8 @@ export default {
|
||||
'login.form.password.errMsg': '密码不能为空',
|
||||
'login.form.login.errMsg': '登录出错,轻刷新重试',
|
||||
'login.form.login.success': '欢迎使用',
|
||||
'login.form.userName.placeholder': '用户名:admin',
|
||||
'login.form.password.placeholder': '密码:admin',
|
||||
'login.form.userName.placeholder': '请输入用户名',
|
||||
'login.form.password.placeholder': '请输入密码',
|
||||
'login.form.rememberPassword': '记住密码',
|
||||
'login.form.forgetPassword': '忘记密码',
|
||||
'login.form.login': '登录',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="content">
|
||||
<a-result class="result" status="404" :subtitle="'not found'"></a-result>
|
||||
<a-result class="result" status="404" :subtitle="'未找到'"></a-result>
|
||||
<div class="operation-row">
|
||||
<a-button key="back" type="primary" @click="back">back</a-button>
|
||||
<a-button key="back" type="primary" @click="back">返回</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
<template>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
class="menu-form"
|
||||
@submit-success="handleSubmit"
|
||||
>
|
||||
<!-- 基本信息 -->
|
||||
<a-divider orientation="left">基本信息</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="code" label="菜单编码" :rules="[{ required: true, message: '请输入菜单编码' }]">
|
||||
<a-input v-model="formData.code" placeholder="请输入菜单编码,如: menu_management" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item field="type" label="菜单类型">
|
||||
<a-select v-model="formData.type">
|
||||
<a-option :value="1">{{ formData.parent_id ? '子菜单' : '根菜单' }}</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item field="title" label="菜单名称" :rules="[{ required: true, message: '请输入菜单名称' }]">
|
||||
<a-input v-model="formData.title" placeholder="请输入中文菜单名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item field="title_en" label="英文名称" :rules="[{ required: true, message: '请输入英文名称' }]">
|
||||
<a-input v-model="formData.title_en" placeholder="请输入英文菜单名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="12">
|
||||
<a-form-item field="menu_path" label="路由路径" :rules="[{ required: true, message: '请输入路由路径' }]">
|
||||
<a-input v-model="formData.menu_path" placeholder="请输入路由路径,如: /ops/menu-management" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<!-- 根菜单才显示图标选择 -->
|
||||
<a-col v-if="!parentId" :span="12">
|
||||
<a-form-item field="menu_icon" label="菜单图标" :rules="[{ required: true, message: '请选择菜单图标' }]">
|
||||
<a-input
|
||||
v-model="formData.menu_icon"
|
||||
placeholder="点击选择图标"
|
||||
readonly
|
||||
@click="iconPickerVisible = true"
|
||||
>
|
||||
<template #prefix>
|
||||
<component v-if="formData.menu_icon" :is="getIconComponent(formData.menu_icon)" :size="18" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="24">
|
||||
<a-form-item field="description" label="菜单描述">
|
||||
<a-textarea v-model="formData.description" placeholder="请输入菜单描述" :auto-size="{ minRows: 3, maxRows: 5 }" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 网页嵌入配置 -->
|
||||
<a-divider orientation="left">网页嵌入配置</a-divider>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="24">
|
||||
<a-form-item field="is_web_page">
|
||||
<a-switch v-model="formData.is_web_page" />
|
||||
<span class="switch-label">是否为嵌入网页</span>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-col v-if="formData.is_web_page" :span="24">
|
||||
<a-form-item field="web_url" label="网页地址">
|
||||
<a-input v-model="formData.web_url" placeholder="请输入要嵌入的网页URL" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="form-actions">
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit" :loading="loading">
|
||||
保存
|
||||
</a-button>
|
||||
<a-button @click="$emit('cancel')">
|
||||
取消
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 图标选择器 -->
|
||||
<a-modal
|
||||
v-model:visible="iconPickerVisible"
|
||||
title="选择图标"
|
||||
:footer="false"
|
||||
width="600px"
|
||||
>
|
||||
<div class="icon-search">
|
||||
<a-input v-model="iconSearch" placeholder="搜索图标" allow-clear>
|
||||
<template #prefix><icon-search /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
<div class="icon-grid">
|
||||
<div
|
||||
v-for="icon in filteredIcons"
|
||||
:key="icon"
|
||||
class="icon-item"
|
||||
:class="{ 'is-selected': formData.menu_icon === icon }"
|
||||
@click="selectIcon(icon)"
|
||||
>
|
||||
<component :is="getIconComponent(icon)" :size="24" class="icon-preview" />
|
||||
<span class="icon-name">{{ icon }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import type { MenuRouteRequest, MenuNode } from '../types'
|
||||
import { COMMON_ICONS, iconNames, getIconComponent } from '../menuIcons'
|
||||
import { IconSearch } from '@tabler/icons-vue'
|
||||
|
||||
const props = defineProps<{
|
||||
mode: 'add' | 'edit' | 'add_child'
|
||||
parentId?: number | null
|
||||
initialValues?: MenuNode | null
|
||||
parentName?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'save', data: MenuRouteRequest): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
const iconPickerVisible = ref(false)
|
||||
const iconSearch = ref('')
|
||||
|
||||
// 表单数据
|
||||
const formData = ref<MenuRouteRequest>({
|
||||
code: '',
|
||||
description: '',
|
||||
menu_icon: '',
|
||||
menu_path: '',
|
||||
parent_id: undefined,
|
||||
title: '',
|
||||
title_en: '',
|
||||
type: 1,
|
||||
is_web_page: false,
|
||||
web_url: '',
|
||||
// 编辑时需要的额外字段
|
||||
id: undefined,
|
||||
identity: undefined,
|
||||
app_id: undefined,
|
||||
sort_key: undefined,
|
||||
created_at: undefined,
|
||||
})
|
||||
|
||||
// 图标列表
|
||||
const iconList = iconNames
|
||||
|
||||
// 过滤图标
|
||||
const filteredIcons = computed(() => {
|
||||
if (!iconSearch.value) return iconList
|
||||
return iconList.filter(icon =>
|
||||
icon.toLowerCase().includes(iconSearch.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
code: [{ required: true, message: '请输入菜单编码' }],
|
||||
title: [{ required: true, message: '请输入菜单名称' }],
|
||||
title_en: [{ required: true, message: '请输入英文名称' }],
|
||||
menu_path: [{ required: true, message: '请输入路由路径' }],
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
code: '',
|
||||
description: '',
|
||||
menu_icon: '',
|
||||
menu_path: '',
|
||||
parent_id: props.parentId || undefined,
|
||||
title: '',
|
||||
title_en: '',
|
||||
type: 1,
|
||||
is_web_page: false,
|
||||
web_url: '',
|
||||
id: undefined,
|
||||
identity: undefined,
|
||||
app_id: undefined,
|
||||
sort_key: undefined,
|
||||
created_at: undefined,
|
||||
}
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 监听 mode 和 initialValues 变化
|
||||
watch(
|
||||
() => [props.mode, props.initialValues] as const,
|
||||
([mode, initialValues]) => {
|
||||
if (mode === 'add' || mode === 'add_child') {
|
||||
resetForm()
|
||||
} else if (mode === 'edit' && initialValues) {
|
||||
// 编辑模式:保留所有原始数据,只更新表单字段
|
||||
formData.value = {
|
||||
id: initialValues.id,
|
||||
identity: initialValues.identity,
|
||||
app_id: initialValues.app_id ?? 2,
|
||||
sort_key: initialValues.sort_key,
|
||||
created_at: initialValues.created_at,
|
||||
code: initialValues.code || '',
|
||||
description: initialValues.description || '',
|
||||
menu_icon: initialValues.menu_icon || '',
|
||||
menu_path: initialValues.menu_path || '',
|
||||
parent_id: initialValues.parent_id,
|
||||
title: initialValues.title || '',
|
||||
title_en: initialValues.title_en || '',
|
||||
type: initialValues.type || 1,
|
||||
is_web_page: initialValues.is_web_page || false,
|
||||
web_url: initialValues.web_url || '',
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 监听父ID变化
|
||||
watch(() => props.parentId, (val) => {
|
||||
if (props.mode === 'add_child') {
|
||||
formData.value.parent_id = val || undefined
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 选择图标
|
||||
const selectIcon = (icon: string) => {
|
||||
formData.value.menu_icon = icon
|
||||
iconPickerVisible.value = false
|
||||
iconSearch.value = ''
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const valid = await formRef.value?.validate()
|
||||
if (valid) return
|
||||
|
||||
emit('save', { ...formData.value })
|
||||
} catch (error) {
|
||||
console.error('Form validation error:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MenuForm',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.menu-form {
|
||||
.switch-label {
|
||||
margin-left: 8px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 8px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: rgb(var(--primary-6));
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border-color: rgb(var(--primary-6));
|
||||
background-color: rgb(var(--primary-1));
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
color: rgb(var(--primary-6));
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-preview {
|
||||
font-size: 24px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.icon-name {
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--color-text-3);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="menu-tree">
|
||||
<MenuTreeItem
|
||||
v-for="node in nodes"
|
||||
:key="node.id"
|
||||
:node="node"
|
||||
:level="1"
|
||||
:expanded-keys="expandedKeys"
|
||||
:selected-key="selectedKey"
|
||||
:dragging-node="draggingNode"
|
||||
@toggle="$emit('toggle', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@add-child="$emit('add-child', $event)"
|
||||
@delete="$emit('delete', $event)"
|
||||
@drag-start="$emit('drag-start', $event)"
|
||||
@drag-end="$emit('drag-end')"
|
||||
@drop="$emit('drop', $event)"
|
||||
/>
|
||||
|
||||
<!-- 根级别拖放区域 -->
|
||||
<div
|
||||
v-if="draggingNode"
|
||||
class="root-drop-zone"
|
||||
:class="{ 'drag-over': isDragOver }"
|
||||
@dragover.prevent="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop.prevent="handleDropToRoot"
|
||||
>
|
||||
<icon-plus-circle /> 拖放到此处设为根级菜单
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import MenuTreeItem from './MenuTreeItem.vue'
|
||||
import type { MenuNode } from '../types'
|
||||
|
||||
const props = defineProps<{
|
||||
nodes: MenuNode[]
|
||||
expandedKeys: Set<number>
|
||||
selectedKey: number | null
|
||||
draggingNode?: MenuNode | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle', nodeId: number): void
|
||||
(e: 'select', node: MenuNode): void
|
||||
(e: 'add-child', parentId: number): void
|
||||
(e: 'delete', node: MenuNode): void
|
||||
(e: 'drag-start', node: MenuNode): void
|
||||
(e: 'drag-end'): void
|
||||
(e: 'drop', data: { dragNode: MenuNode; targetNode: MenuNode | null; position: 'before' | 'after' | 'inside' | 'root' }): void
|
||||
}>()
|
||||
|
||||
const isDragOver = ref(false)
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const handleDropToRoot = () => {
|
||||
isDragOver.value = false
|
||||
|
||||
if (props.draggingNode) {
|
||||
// 拖放到根级别
|
||||
emit('drop', {
|
||||
dragNode: props.draggingNode,
|
||||
targetNode: null,
|
||||
position: 'root'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MenuTree',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.menu-tree {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.root-drop-zone {
|
||||
margin-top: 8px;
|
||||
padding: 16px;
|
||||
border: 2px dashed var(--color-border-2);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
color: var(--color-text-3);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover, &.drag-over {
|
||||
border-color: rgb(var(--primary-6));
|
||||
color: rgb(var(--primary-6));
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="menu-tree-item">
|
||||
<div
|
||||
class="menu-item-content"
|
||||
:class="{
|
||||
'is-selected': selectedKey === node.id,
|
||||
'is-hovered': isHovered,
|
||||
'drag-over': isDragOver
|
||||
}"
|
||||
:style="{ paddingLeft: `${level * 16}px` }"
|
||||
draggable="true"
|
||||
@click="handleSelect"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
@dragstart="handleDragStart"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
@dragend="handleDragEnd"
|
||||
>
|
||||
<!-- 展开/折叠按钮 -->
|
||||
<span class="expand-btn" @click.stop="handleToggle">
|
||||
<icon-right v-if="!isExpanded && hasChildren" />
|
||||
<icon-down v-else-if="isExpanded && hasChildren" />
|
||||
<span v-else class="placeholder"></span>
|
||||
</span>
|
||||
|
||||
<!-- 拖拽手柄 -->
|
||||
<span class="drag-handle">
|
||||
<icon-drag-dot-vertical />
|
||||
</span>
|
||||
|
||||
<!-- 菜单图标 -->
|
||||
<span v-if="node.menu_icon" class="menu-icon">
|
||||
<component :is="getIcon(node.menu_icon)" />
|
||||
</span>
|
||||
|
||||
<!-- 菜单标题 -->
|
||||
<span class="menu-title">{{ node.title }}</span>
|
||||
|
||||
<!-- 分组标识 -->
|
||||
<a-tag v-if="!node.menu_path" size="small" color="gray">分组</a-tag>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<span v-show="isHovered" class="action-btns">
|
||||
<!-- 只有一级菜单(根菜单)才显示添加按钮 -->
|
||||
<a-tooltip v-if="level === 1" content="添加子菜单">
|
||||
<a-button type="text" size="mini" @click.stop="handleAddChild">
|
||||
<template #icon><icon-plus /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip content="删除">
|
||||
<a-button type="text" size="mini" status="danger" @click.stop="handleDelete">
|
||||
<template #icon><icon-delete /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 子菜单 -->
|
||||
<div v-if="isExpanded && hasChildren" class="menu-children">
|
||||
<MenuTreeItem
|
||||
v-for="child in node.children"
|
||||
:key="child.id"
|
||||
:node="child"
|
||||
:level="level + 1"
|
||||
:expanded-keys="expandedKeys"
|
||||
:selected-key="selectedKey"
|
||||
:dragging-node="draggingNode"
|
||||
@toggle="$emit('toggle', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
@add-child="$emit('add-child', $event)"
|
||||
@delete="$emit('delete', $event)"
|
||||
@drag-start="$emit('drag-start', $event)"
|
||||
@drag-end="$emit('drag-end')"
|
||||
@drop="$emit('drop', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import type { MenuNode } from '../types'
|
||||
import { getIconComponent } from '../menuIcons'
|
||||
|
||||
const props = defineProps<{
|
||||
node: MenuNode
|
||||
level: number
|
||||
expandedKeys: Set<number>
|
||||
selectedKey: number | null
|
||||
draggingNode?: MenuNode | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'toggle', nodeId: number): void
|
||||
(e: 'select', node: MenuNode): void
|
||||
(e: 'add-child', parentId: number): void
|
||||
(e: 'delete', node: MenuNode): void
|
||||
(e: 'drag-start', node: MenuNode): void
|
||||
(e: 'drag-end'): void
|
||||
(e: 'drop', data: { dragNode: MenuNode; targetNode: MenuNode; position: 'before' | 'after' | 'inside' }): void
|
||||
}>()
|
||||
|
||||
const isHovered = ref(false)
|
||||
const isDragOver = ref(false)
|
||||
|
||||
const isExpanded = computed(() => props.expandedKeys.has(props.node.id))
|
||||
const hasChildren = computed(() => props.node.children && props.node.children.length > 0)
|
||||
const isDraggingThis = computed(() => props.draggingNode?.id === props.node.id)
|
||||
|
||||
const handleToggle = () => {
|
||||
if (hasChildren.value) {
|
||||
emit('toggle', props.node.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelect = () => {
|
||||
emit('select', props.node)
|
||||
}
|
||||
|
||||
const handleAddChild = () => {
|
||||
emit('add-child', props.node.id)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
emit('delete', props.node)
|
||||
}
|
||||
|
||||
// 拖拽相关
|
||||
const handleDragStart = (e: DragEvent) => {
|
||||
e.dataTransfer!.effectAllowed = 'move'
|
||||
emit('drag-start', props.node)
|
||||
// 添加拖拽样式
|
||||
const target = e.target as HTMLElement
|
||||
setTimeout(() => {
|
||||
target.style.opacity = '0.5'
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.dataTransfer!.dropEffect = 'move'
|
||||
isDragOver.value = true
|
||||
}
|
||||
|
||||
const handleDragLeave = () => {
|
||||
isDragOver.value = false
|
||||
}
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
isDragOver.value = false
|
||||
|
||||
if (props.draggingNode && props.draggingNode.id !== props.node.id) {
|
||||
// 判断放置位置
|
||||
const rect = (e.target as HTMLElement).getBoundingClientRect()
|
||||
const y = e.clientY - rect.top
|
||||
const height = rect.height
|
||||
|
||||
let position: 'before' | 'after' | 'inside' = 'inside'
|
||||
if (y < height * 0.25) {
|
||||
position = 'before'
|
||||
} else if (y > height * 0.75) {
|
||||
position = 'after'
|
||||
} else {
|
||||
position = 'inside'
|
||||
}
|
||||
|
||||
emit('drop', {
|
||||
dragNode: props.draggingNode,
|
||||
targetNode: props.node,
|
||||
position
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragEnd = (e: DragEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
target.style.opacity = '1'
|
||||
emit('drag-end')
|
||||
}
|
||||
|
||||
// 获取图标组件
|
||||
const getIcon = (iconName: string) => {
|
||||
return getIconComponent(iconName)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MenuTreeItem',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.menu-tree-item {
|
||||
.menu-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid transparent;
|
||||
|
||||
&:hover, &.is-hovered {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background-color: rgb(var(--primary-1));
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&.drag-over {
|
||||
border-color: rgb(var(--primary-6));
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 4px;
|
||||
color: var(--color-text-3);
|
||||
|
||||
.placeholder {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 4px;
|
||||
color: var(--color-text-4);
|
||||
cursor: grab;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 8px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-children {
|
||||
// 子菜单样式
|
||||
}
|
||||
}
|
||||
</style>
|
||||
463
src/views/ops/pages/system-settings/menu-management/index.vue
Normal file
463
src/views/ops/pages/system-settings/menu-management/index.vue
Normal file
@@ -0,0 +1,463 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.ops', 'menu.ops.systemSettings', 'menu.ops.systemSettings.menuManagement']" />
|
||||
<a-row :gutter="16" class="menu-container">
|
||||
<!-- 左侧: 菜单树 -->
|
||||
<a-col :xs="24" :md="10" :lg="8">
|
||||
<a-card class="menu-tree-card" :bordered="false">
|
||||
<!-- 头部操作区 -->
|
||||
<template #title>
|
||||
<div class="card-header">
|
||||
<span>菜单管理</span>
|
||||
<a-button type="primary" size="small" @click="handleAddRootMenu">
|
||||
<template #icon><icon-plus /></template>
|
||||
添加根菜单
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<a-alert class="menu-tip" type="info" :show-icon="true">
|
||||
点击菜单项可编辑,悬停显示操作按钮
|
||||
</a-alert>
|
||||
|
||||
<!-- 菜单树 -->
|
||||
<a-spin :loading="loading" class="menu-tree-spin">
|
||||
<div class="menu-tree-wrapper">
|
||||
<MenuTree
|
||||
v-if="rootItems.length > 0"
|
||||
:nodes="rootItems"
|
||||
:expanded-keys="expandedKeys"
|
||||
:selected-key="selectedNode?.id || null"
|
||||
:dragging-node="draggingNode"
|
||||
@toggle="handleToggleExpand"
|
||||
@select="handleSelectNode"
|
||||
@add-child="handleAddChildMenu"
|
||||
@delete="handleDeleteClick"
|
||||
@drag-start="handleDragStart"
|
||||
@drag-end="handleDragEnd"
|
||||
@drop="handleDrop"
|
||||
/>
|
||||
<a-empty v-else description="暂无菜单数据" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧: 编辑表单 -->
|
||||
<a-col :xs="24" :md="14" :lg="16">
|
||||
<a-card class="menu-edit-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="edit-header">
|
||||
<span>{{ getEditTitle() }}</span>
|
||||
<span v-if="isAddingChild || (isEditing && selectedNode?.parent_id)" class="parent-info">
|
||||
父菜单:
|
||||
<a-tag color="arcoblue">
|
||||
{{ isEditing && selectedNode?.parent_id ? getParentMenuName(selectedNode.parent_id) : getParentMenuName(parentId) }}
|
||||
</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 编辑表单 -->
|
||||
<MenuForm
|
||||
v-if="isEditing || isAdding || isAddingChild"
|
||||
:mode="getEditMode()"
|
||||
:parent-id="parentId"
|
||||
:initial-values="selectedNode"
|
||||
:parent-name="getParentMenuName(parentId)"
|
||||
@save="handleSaveMenu"
|
||||
@cancel="handleCancelEdit"
|
||||
/>
|
||||
|
||||
<a-empty v-else description="请从左侧选择一个菜单项进行编辑" class="empty-state" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 删除确认对话框 -->
|
||||
<a-modal
|
||||
v-model:visible="deleteConfirmVisible"
|
||||
title="删除确认"
|
||||
@ok="handleConfirmDelete"
|
||||
@cancel="deleteConfirmVisible = false"
|
||||
>
|
||||
<p>确定要删除菜单 "{{ nodeToDelete?.title }}" 吗?</p>
|
||||
<p v-if="nodeToDelete?.children?.length" class="warning-text">
|
||||
<icon-exclamation-circle-fill /> 注意:该菜单下有子菜单,删除后子菜单也会被删除!
|
||||
</p>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { MenuNode, MenuRouteRequest } from './types'
|
||||
import { fetchMenu, createMenu, modifyMenu, deleteMenu, updateMenuOrder } from '@/api/module/pmn'
|
||||
import { buildTree } from '@/utils/tree'
|
||||
import MenuTree from './components/MenuTree.vue'
|
||||
import MenuForm from './components/MenuForm.vue'
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const menuItems = ref<MenuRouteRequest[]>([])
|
||||
const expandedKeys = ref<Set<number>>(new Set())
|
||||
const selectedNode = ref<MenuNode | null>(null)
|
||||
|
||||
// 编辑状态
|
||||
const isEditing = ref(false)
|
||||
const isAdding = ref(false)
|
||||
const isAddingChild = ref(false)
|
||||
const parentId = ref<number | null>(null)
|
||||
|
||||
// 删除确认
|
||||
const deleteConfirmVisible = ref(false)
|
||||
const nodeToDelete = ref<MenuNode | null>(null)
|
||||
|
||||
// 拖拽状态
|
||||
const draggingNode = ref<MenuNode | null>(null)
|
||||
|
||||
// 计算属性 - 使用公共 buildTree 函数构建树
|
||||
const treeData = computed(() => buildTree<MenuNode>(menuItems.value as MenuNode[], { orderKey: 'order' }))
|
||||
const rootItems = computed(() => treeData.value.rootItems)
|
||||
const itemMap = computed(() => treeData.value.itemMap)
|
||||
|
||||
// 加载菜单数据
|
||||
const loadMenuItems = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await fetchMenu({ page: 1, size: 10000 })
|
||||
if (res?.code === 0) {
|
||||
menuItems.value = res.details?.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load menu items:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换节点展开/折叠
|
||||
const handleToggleExpand = (nodeId: number) => {
|
||||
if (expandedKeys.value.has(nodeId)) {
|
||||
expandedKeys.value.delete(nodeId)
|
||||
} else {
|
||||
expandedKeys.value.add(nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择节点
|
||||
const handleSelectNode = (node: MenuNode) => {
|
||||
selectedNode.value = node
|
||||
isEditing.value = true
|
||||
isAdding.value = false
|
||||
isAddingChild.value = false
|
||||
}
|
||||
|
||||
// 添加根菜单
|
||||
const handleAddRootMenu = () => {
|
||||
selectedNode.value = null
|
||||
isEditing.value = false
|
||||
isAdding.value = true
|
||||
isAddingChild.value = false
|
||||
parentId.value = null
|
||||
}
|
||||
|
||||
// 添加子菜单
|
||||
const handleAddChildMenu = (pId: number) => {
|
||||
selectedNode.value = null
|
||||
isEditing.value = false
|
||||
isAdding.value = false
|
||||
isAddingChild.value = true
|
||||
parentId.value = pId
|
||||
}
|
||||
|
||||
// 编辑菜单
|
||||
const handleEditMenu = (node: MenuNode) => {
|
||||
selectedNode.value = node
|
||||
isEditing.value = true
|
||||
isAdding.value = false
|
||||
isAddingChild.value = false
|
||||
parentId.value = node.parent_id || null
|
||||
}
|
||||
|
||||
// 删除点击
|
||||
const handleDeleteClick = (node: MenuNode) => {
|
||||
nodeToDelete.value = node
|
||||
deleteConfirmVisible.value = true
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!nodeToDelete.value?.id) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
await deleteMenu({ id: nodeToDelete.value.id })
|
||||
Message.success('删除成功')
|
||||
|
||||
// 如果删除的是当前选中的节点,清空编辑状态
|
||||
if (selectedNode.value?.id === nodeToDelete.value.id) {
|
||||
selectedNode.value = null
|
||||
isEditing.value = false
|
||||
isAdding.value = false
|
||||
isAddingChild.value = false
|
||||
parentId.value = null
|
||||
}
|
||||
|
||||
deleteConfirmVisible.value = false
|
||||
nodeToDelete.value = null
|
||||
await loadMenuItems()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete menu:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存菜单
|
||||
const handleSaveMenu = async (menuData: MenuRouteRequest) => {
|
||||
try {
|
||||
loading.value = true
|
||||
if (isEditing.value && selectedNode.value?.id) {
|
||||
await modifyMenu({ ...menuData, id: selectedNode.value.id })
|
||||
} else {
|
||||
await createMenu(menuData)
|
||||
}
|
||||
Message.success('保存成功')
|
||||
|
||||
// 重置状态
|
||||
isEditing.value = false
|
||||
isAdding.value = false
|
||||
isAddingChild.value = false
|
||||
await loadMenuItems()
|
||||
} catch (error) {
|
||||
console.error('Failed to save menu:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
const handleCancelEdit = () => {
|
||||
isEditing.value = false
|
||||
isAdding.value = false
|
||||
isAddingChild.value = false
|
||||
selectedNode.value = null
|
||||
}
|
||||
|
||||
// 获取编辑模式
|
||||
const getEditMode = (): 'add' | 'edit' | 'add_child' => {
|
||||
if (isEditing.value) return 'edit'
|
||||
if (isAddingChild.value) return 'add_child'
|
||||
return 'add'
|
||||
}
|
||||
|
||||
// 获取编辑区标题
|
||||
const getEditTitle = () => {
|
||||
if (isEditing.value && selectedNode.value) {
|
||||
return `编辑: ${selectedNode.value.title}`
|
||||
}
|
||||
if (isAddingChild.value) {
|
||||
const parentName = getParentMenuName(parentId.value)
|
||||
return `为 "${parentName}" 添加子菜单`
|
||||
}
|
||||
if (isAdding.value) return '添加根菜单'
|
||||
return '详情'
|
||||
}
|
||||
|
||||
// 获取父菜单名称
|
||||
const getParentMenuName = (pId?: number | null) => {
|
||||
const map = itemMap.value
|
||||
if (!pId || !map.has(pId)) return ''
|
||||
const item = map.get(pId)
|
||||
return item?.title || ''
|
||||
}
|
||||
|
||||
// 拖拽开始
|
||||
const handleDragStart = (node: MenuNode) => {
|
||||
draggingNode.value = node
|
||||
}
|
||||
|
||||
// 拖拽结束
|
||||
const handleDragEnd = () => {
|
||||
draggingNode.value = null
|
||||
}
|
||||
|
||||
// 处理放置
|
||||
const handleDrop = async (data: { dragNode: MenuNode; targetNode: MenuNode | null; position: 'before' | 'after' | 'inside' | 'root' }) => {
|
||||
const { dragNode, targetNode, position } = data
|
||||
|
||||
if (!dragNode.id) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
if (position === 'root' && !targetNode) {
|
||||
// 拖放到根级别
|
||||
const { children, ...dragData } = dragNode
|
||||
await modifyMenu({
|
||||
...dragData,
|
||||
parent_id: null
|
||||
} as MenuRouteRequest)
|
||||
} else if (targetNode) {
|
||||
if (position === 'inside') {
|
||||
// 拖放到某个节点内部,成为其子节点
|
||||
const { children, ...dragData } = dragNode
|
||||
await modifyMenu({
|
||||
...dragData,
|
||||
parent_id: targetNode.id
|
||||
} as MenuRouteRequest)
|
||||
} else {
|
||||
// 拖放到某个节点前后,需要更新排序
|
||||
const sortList: { pmn_id: number; sort_key: number }[] = []
|
||||
let sortKey = 1
|
||||
|
||||
// 获取同级节点
|
||||
const siblings = menuItems.value
|
||||
.filter(item => item.parent_id === targetNode.parent_id)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
|
||||
siblings.forEach((item, index) => {
|
||||
if (item.id === targetNode.id) {
|
||||
if (position === 'before') {
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
} else {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
}
|
||||
} else if (item.id !== dragNode.id) {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
}
|
||||
})
|
||||
|
||||
// 更新拖拽节点的 parent_id
|
||||
const { children, ...dragData } = dragNode
|
||||
await modifyMenu({
|
||||
...dragData,
|
||||
parent_id: targetNode.parent_id || null
|
||||
} as MenuRouteRequest)
|
||||
|
||||
// 更新排序
|
||||
if (sortList.length > 0) {
|
||||
await updateMenuOrder(sortList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message.success('移动成功')
|
||||
await loadMenuItems()
|
||||
} catch (error) {
|
||||
console.error('Failed to move menu:', error)
|
||||
Message.error('移动失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
draggingNode.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 递归收集子节点
|
||||
const collectChildren = (
|
||||
items: MenuRouteRequest[],
|
||||
parentId: number,
|
||||
sortList: { pmn_id: number; sort_key: number }[],
|
||||
getSortKey: () => number
|
||||
) => {
|
||||
const children = items
|
||||
.filter((item: MenuRouteRequest) => item.parent_id === parentId)
|
||||
.sort((a: MenuRouteRequest, b: MenuRouteRequest) => (a.order || 0) - (b.order || 0))
|
||||
|
||||
children.forEach((item: MenuRouteRequest) => {
|
||||
if (item.id) {
|
||||
sortList.push({ pmn_id: item.id, sort_key: getSortKey() })
|
||||
collectChildren(items, item.id, sortList, getSortKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
onMounted(() => {
|
||||
loadMenuItems()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'MenuManagement',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
height: calc(100vh - 300px);
|
||||
> div {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-tree-card,
|
||||
.menu-edit-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-tip {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.menu-tree-spin {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-tree-wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.edit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.parent-info {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
color: rgb(var(--warning-6));
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
296
src/views/ops/pages/system-settings/menu-management/menuIcons.ts
Normal file
296
src/views/ops/pages/system-settings/menu-management/menuIcons.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
// 菜单图标集合 - 使用 @tabler/icons-vue
|
||||
import type { Component } from 'vue';
|
||||
import {
|
||||
IconDashboard, IconHome, IconSettings, IconUser, IconUsers, IconUserCircle,
|
||||
IconBuilding, IconFileText, IconCoin, IconChartBar, IconChartPie, IconList,
|
||||
IconShield, IconWorld, IconLanguage, IconHelp, IconInfoCircle, IconFolder,
|
||||
IconDatabase, IconMenu, IconCircle, IconSearch, IconFolderOff,
|
||||
IconAlertTriangle,
|
||||
// Business & Finance
|
||||
Icon2fa, IconAbacus, IconAccessPoint, IconActivity, IconAd, IconAdjustments,
|
||||
IconAlarm, IconAlertOctagon, IconAnalyze, IconAward, IconBarcode, IconBuildingBank,
|
||||
IconCalculator, IconCalendar, IconCash, IconChartInfographic, IconCreditCard,
|
||||
// Communication & Social
|
||||
IconMail, IconMessage, IconMessages, IconPhone, IconShare, IconSocial,
|
||||
IconBrandFacebook, IconBrandTwitter, IconBrandLinkedin, IconBrandInstagram,
|
||||
IconBrandWhatsapp, IconBrandTelegram, IconBrandSkype, IconBrandZoom,
|
||||
// Development & Technology
|
||||
IconCode, IconBraces, IconBug, IconDeviceDesktop,
|
||||
IconDeviceLaptop, IconDeviceMobile, IconDeviceTablet, IconGitBranch, IconTerminal,
|
||||
IconServer, IconWifi, IconBrandGithub, IconBrandVscode, IconBrandDocker,
|
||||
IconCloud,
|
||||
// Documents & Files
|
||||
IconFile, IconFiles, IconFileZip, IconFile3d,
|
||||
IconFileDownload, IconFileUpload, IconFolderPlus, IconBook,
|
||||
IconBooks, IconBookmark, IconClipboard, IconNotes, IconPrinter,
|
||||
// Interface Elements
|
||||
IconCheckbox, IconForms, IconLayout,
|
||||
IconMenu2, IconRadio, IconSelect, IconSwitch, IconTable,
|
||||
IconToggleLeft, IconToggleRight, IconWindowMaximize,
|
||||
// Media & Entertainment
|
||||
IconCamera, IconPhoto, IconVideo, IconMusic, IconPlayerPlay, IconPlayerPause,
|
||||
IconPlayerStop, IconMovie, IconMicrophone, IconVolume, IconHeadphones,
|
||||
IconBrush, IconPalette, IconPencil, IconPaint, IconColorPicker,
|
||||
// Navigation & Maps
|
||||
IconMap, IconMapPin, IconCompass, IconDirection, IconGps, IconLocation,
|
||||
IconNavigation, IconRoute, IconRoad, IconTrain, IconPlane, IconCar,
|
||||
IconBike, IconWalk, IconRun, IconSwimming,
|
||||
// Shopping & E-commerce
|
||||
IconShoppingCart, IconShoppingBag, IconBasket, IconGift, IconDiscount,
|
||||
IconTag, IconTags, IconReceipt, IconTruck, IconPackage, IconBox,
|
||||
// Weather & Nature
|
||||
IconSun, IconMoon, IconCloudRain, IconCloudSnow, IconWind,
|
||||
IconTemperature, IconUmbrella, IconPlant, IconTree, IconLeaf, IconFlower,
|
||||
// Additional UI Elements
|
||||
IconEdit, IconCopy, IconArrowBack, IconArrowForward, IconRefresh, IconRotate,
|
||||
IconZoomIn, IconZoomOut, IconFilter, IconSortAscending,
|
||||
IconSortDescending, IconColumns, IconLayoutGrid,
|
||||
// Security & Access
|
||||
IconLock, IconKey, IconPassword, IconFingerprint, IconShieldLock,
|
||||
IconCertificate, IconId, IconBadge,
|
||||
// Time & Date
|
||||
IconClock, IconCalendarEvent, IconCalendarTime, IconCalendarStats,
|
||||
IconHourglass, IconHistory, IconTimeline, IconAlarmPlus,
|
||||
// Health & Medical
|
||||
IconHeartbeat, IconStethoscope, IconPill, IconVirus, IconVaccine,
|
||||
IconEmergencyBed, IconAmbulance, IconMedicalCross, IconDna,
|
||||
// Analytics & Data
|
||||
IconChartArea, IconChartArrows, IconChartCandle,
|
||||
IconChartLine, IconPresentationAnalytics,
|
||||
IconReportAnalytics, IconTrendingUp, IconTrendingDown,
|
||||
IconSend, IconUpload, IconPlug, IconNotebook, IconTools, IconPoint,
|
||||
} from '@tabler/icons-vue';
|
||||
|
||||
// 预选图标列表
|
||||
export const COMMON_ICONS: Record<string, Component> = {
|
||||
// 常用图标
|
||||
Dashboard: IconDashboard,
|
||||
Home: IconHome,
|
||||
Settings: IconSettings,
|
||||
AccountCircle: IconUserCircle,
|
||||
People: IconUsers,
|
||||
Person: IconUser,
|
||||
Groups: IconUsers,
|
||||
Store: IconBuilding,
|
||||
Description: IconFileText,
|
||||
AttachMoney: IconCoin,
|
||||
BarChart: IconChartBar,
|
||||
Insights: IconChartPie,
|
||||
Security: IconShield,
|
||||
Public: IconWorld,
|
||||
Language: IconLanguage,
|
||||
Help: IconHelp,
|
||||
Folder: IconFolder,
|
||||
Storage: IconDatabase,
|
||||
Menu: IconMenu,
|
||||
Link: IconCircle,
|
||||
Search: IconSearch,
|
||||
List: IconList,
|
||||
Building: IconBuilding,
|
||||
World: IconWorld,
|
||||
Coin: IconCoin,
|
||||
Users: IconUsers,
|
||||
// Business & Finance
|
||||
TwoFactor: Icon2fa,
|
||||
Abacus: IconAbacus,
|
||||
AccessPoint: IconAccessPoint,
|
||||
Activity: IconActivity,
|
||||
Advertisement: IconAd,
|
||||
Adjustments: IconAdjustments,
|
||||
Alarm: IconAlarm,
|
||||
AlertOctagon: IconAlertOctagon,
|
||||
Analyze: IconAnalyze,
|
||||
Award: IconAward,
|
||||
Barcode: IconBarcode,
|
||||
Bank: IconBuildingBank,
|
||||
Calculator: IconCalculator,
|
||||
Calendar: IconCalendar,
|
||||
Cash: IconCash,
|
||||
CreditCard: IconCreditCard,
|
||||
// Communication & Social
|
||||
Mail: IconMail,
|
||||
Message: IconMessage,
|
||||
Messages: IconMessages,
|
||||
Phone: IconPhone,
|
||||
Share: IconShare,
|
||||
Social: IconSocial,
|
||||
Facebook: IconBrandFacebook,
|
||||
Twitter: IconBrandTwitter,
|
||||
LinkedIn: IconBrandLinkedin,
|
||||
Instagram: IconBrandInstagram,
|
||||
WhatsApp: IconBrandWhatsapp,
|
||||
Telegram: IconBrandTelegram,
|
||||
Skype: IconBrandSkype,
|
||||
Zoom: IconBrandZoom,
|
||||
// Development & Technology
|
||||
Code: IconCode,
|
||||
Braces: IconBraces,
|
||||
Bug: IconBug,
|
||||
Cloud: IconCloud,
|
||||
Desktop: IconDeviceDesktop,
|
||||
Laptop: IconDeviceLaptop,
|
||||
Mobile: IconDeviceMobile,
|
||||
Tablet: IconDeviceTablet,
|
||||
Git: IconGitBranch,
|
||||
Terminal: IconTerminal,
|
||||
Server: IconServer,
|
||||
Wifi: IconWifi,
|
||||
Github: IconBrandGithub,
|
||||
VSCode: IconBrandVscode,
|
||||
Docker: IconBrandDocker,
|
||||
// Documents & Files
|
||||
File: IconFile,
|
||||
Files: IconFiles,
|
||||
FileZip: IconFileZip,
|
||||
FilePdf: IconFile3d,
|
||||
FileDownload: IconFileDownload,
|
||||
FileUpload: IconFileUpload,
|
||||
FolderPlus: IconFolderPlus,
|
||||
Book: IconBook,
|
||||
Books: IconBooks,
|
||||
Bookmark: IconBookmark,
|
||||
Clipboard: IconClipboard,
|
||||
Notes: IconNotes,
|
||||
Printer: IconPrinter,
|
||||
// Interface Elements
|
||||
Checkbox: IconCheckbox,
|
||||
Forms: IconForms,
|
||||
Layout: IconLayout,
|
||||
Menu2: IconMenu2,
|
||||
Radio: IconRadio,
|
||||
Select: IconSelect,
|
||||
Switch: IconSwitch,
|
||||
Table: IconTable,
|
||||
ToggleLeft: IconToggleLeft,
|
||||
ToggleRight: IconToggleRight,
|
||||
WindowMaximize: IconWindowMaximize,
|
||||
// Media & Entertainment
|
||||
Camera: IconCamera,
|
||||
Photo: IconPhoto,
|
||||
Video: IconVideo,
|
||||
Music: IconMusic,
|
||||
Play: IconPlayerPlay,
|
||||
Pause: IconPlayerPause,
|
||||
Stop: IconPlayerStop,
|
||||
Movie: IconMovie,
|
||||
Microphone: IconMicrophone,
|
||||
Volume: IconVolume,
|
||||
Headphones: IconHeadphones,
|
||||
Brush: IconBrush,
|
||||
Palette: IconPalette,
|
||||
Pencil: IconPencil,
|
||||
Paint: IconPaint,
|
||||
ColorPicker: IconColorPicker,
|
||||
// Navigation & Maps
|
||||
Map: IconMap,
|
||||
MapPin: IconMapPin,
|
||||
Compass: IconCompass,
|
||||
Direction: IconDirection,
|
||||
GPS: IconGps,
|
||||
Location: IconLocation,
|
||||
Navigation: IconNavigation,
|
||||
Route: IconRoute,
|
||||
Road: IconRoad,
|
||||
Train: IconTrain,
|
||||
Plane: IconPlane,
|
||||
Car: IconCar,
|
||||
Bike: IconBike,
|
||||
Walk: IconWalk,
|
||||
Run: IconRun,
|
||||
Swimming: IconSwimming,
|
||||
// Shopping & E-commerce
|
||||
ShoppingCart: IconShoppingCart,
|
||||
ShoppingBag: IconShoppingBag,
|
||||
Basket: IconBasket,
|
||||
Gift: IconGift,
|
||||
Discount: IconDiscount,
|
||||
Tag: IconTag,
|
||||
Tags: IconTags,
|
||||
Receipt: IconReceipt,
|
||||
Truck: IconTruck,
|
||||
Package: IconPackage,
|
||||
Box: IconBox,
|
||||
// Weather & Nature
|
||||
Sun: IconSun,
|
||||
Moon: IconMoon,
|
||||
CloudRain: IconCloudRain,
|
||||
CloudSnow: IconCloudSnow,
|
||||
Wind: IconWind,
|
||||
Temperature: IconTemperature,
|
||||
Umbrella: IconUmbrella,
|
||||
Plant: IconPlant,
|
||||
Tree: IconTree,
|
||||
Leaf: IconLeaf,
|
||||
Flower: IconFlower,
|
||||
// Additional UI Elements
|
||||
Edit: IconEdit,
|
||||
Copy: IconCopy,
|
||||
Undo: IconArrowBack,
|
||||
Redo: IconArrowForward,
|
||||
Refresh: IconRefresh,
|
||||
Rotate: IconRotate,
|
||||
ZoomIn: IconZoomIn,
|
||||
ZoomOut: IconZoomOut,
|
||||
Filter: IconFilter,
|
||||
Sort: IconSortAscending,
|
||||
SortAscending: IconSortAscending,
|
||||
SortDescending: IconSortDescending,
|
||||
Columns: IconColumns,
|
||||
Grid: IconLayoutGrid,
|
||||
// Security & Access
|
||||
Lock: IconLock,
|
||||
Key: IconKey,
|
||||
Password: IconPassword,
|
||||
Fingerprint: IconFingerprint,
|
||||
ShieldLock: IconShieldLock,
|
||||
Certificate: IconCertificate,
|
||||
ID: IconId,
|
||||
Badge: IconBadge,
|
||||
// Time & Date
|
||||
Clock: IconClock,
|
||||
CalendarEvent: IconCalendarEvent,
|
||||
CalendarTime: IconCalendarTime,
|
||||
CalendarStats: IconCalendarStats,
|
||||
Hourglass: IconHourglass,
|
||||
History: IconHistory,
|
||||
Timeline: IconTimeline,
|
||||
AlarmPlus: IconAlarmPlus,
|
||||
// Health & Medical
|
||||
Heartbeat: IconHeartbeat,
|
||||
Stethoscope: IconStethoscope,
|
||||
Pill: IconPill,
|
||||
Virus: IconVirus,
|
||||
Vaccine: IconVaccine,
|
||||
EmergencyBed: IconEmergencyBed,
|
||||
Ambulance: IconAmbulance,
|
||||
MedicalCross: IconMedicalCross,
|
||||
DNA: IconDna,
|
||||
// Analytics & Data
|
||||
ChartArea: IconChartArea,
|
||||
ChartArrows: IconChartArrows,
|
||||
ChartCandle: IconChartCandle,
|
||||
ChartInfographic: IconChartInfographic,
|
||||
ChartLine: IconChartLine,
|
||||
PresentationAnalytics: IconPresentationAnalytics,
|
||||
ReportAnalytics: IconReportAnalytics,
|
||||
TrendingUp: IconTrendingUp,
|
||||
TrendingDown: IconTrendingDown,
|
||||
Send: IconSend,
|
||||
InfoCircle: IconInfoCircle,
|
||||
Upload: IconUpload,
|
||||
Plug: IconPlug,
|
||||
Notebook: IconNotebook,
|
||||
Tools: IconTools,
|
||||
Point: IconPoint,
|
||||
};
|
||||
|
||||
// 导出图标名称列表
|
||||
export const iconNames = Object.keys(COMMON_ICONS);
|
||||
|
||||
// 导出类型
|
||||
export type IconName = keyof typeof COMMON_ICONS;
|
||||
|
||||
// 获取图标组件
|
||||
export const getIconComponent = (iconName: string) => {
|
||||
return COMMON_ICONS[iconName];
|
||||
};
|
||||
14
src/views/ops/pages/system-settings/menu-management/types.ts
Normal file
14
src/views/ops/pages/system-settings/menu-management/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// 从API导入类型
|
||||
import type { MenuItem as MenuItemType } from '@/api/module/pmn'
|
||||
|
||||
// 菜单路由请求接口
|
||||
export interface MenuRouteRequest extends MenuItemType {}
|
||||
|
||||
// 菜单节点类型
|
||||
export interface MenuNode extends MenuRouteRequest {
|
||||
id: number
|
||||
children: MenuNode[]
|
||||
}
|
||||
|
||||
// 图标名称类型
|
||||
export type IconName = string
|
||||
270
src/views/ops/pages/system-settings/system-logs/index.vue
Normal file
270
src/views/ops/pages/system-settings/system-logs/index.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.ops', 'menu.ops.systemSettings', 'menu.ops.systemSettings.systemLogs']" />
|
||||
|
||||
<!-- 使用 SearchTable 公共组件 -->
|
||||
<SearchTable
|
||||
:form-model="formModel"
|
||||
:form-items="formItems"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="系统日志"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
download-button-text="导出"
|
||||
refresh-tooltip-text="刷新数据"
|
||||
density-tooltip-text="表格密度"
|
||||
column-setting-tooltip-text="列设置"
|
||||
@search="handleSearch"
|
||||
@reset="handleReset"
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
@download="handleDownload"
|
||||
>
|
||||
<!-- 表格自定义列:日志级别 -->
|
||||
<template #level="{ record }">
|
||||
<a-tag :color="getLevelColor(record.level)">
|
||||
{{ record.level }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 表格自定义列:序号 -->
|
||||
<template #index="{ rowIndex }">
|
||||
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
|
||||
</template>
|
||||
|
||||
<!-- 表格自定义列:操作 -->
|
||||
<template #operations="{ record }">
|
||||
<a-button type="text" size="small" @click="handleView(record)">
|
||||
查看
|
||||
</a-button>
|
||||
</template>
|
||||
</SearchTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, reactive } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
// 定义表格数据类型
|
||||
interface LogRecord {
|
||||
id: number
|
||||
level: string
|
||||
module: string
|
||||
content: string
|
||||
operator: string
|
||||
ip: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// 模拟数据生成
|
||||
const generateMockData = (count: number): LogRecord[] => {
|
||||
const levels = ['INFO', 'WARN', 'ERROR', 'DEBUG']
|
||||
const modules = ['用户管理', '权限管理', '系统配置', '数据备份', '登录认证']
|
||||
const operators = ['管理员', '张三', '李四', '系统', '定时任务']
|
||||
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
id: i + 1,
|
||||
level: levels[i % levels.length],
|
||||
module: modules[i % modules.length],
|
||||
content: `日志内容描述 ${i + 1}`,
|
||||
operator: operators[i % operators.length],
|
||||
ip: `192.168.${Math.floor(i / 255)}.${i % 255}`,
|
||||
createdAt: new Date(Date.now() - i * 3600000).toLocaleString('zh-CN'),
|
||||
}))
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<LogRecord[]>([])
|
||||
const formModel = ref({
|
||||
level: '',
|
||||
module: '',
|
||||
operator: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
|
||||
// 表单项配置
|
||||
const formItems = computed<FormItem[]>(() => [
|
||||
{
|
||||
field: 'level',
|
||||
label: '日志级别',
|
||||
type: 'select',
|
||||
placeholder: '请选择日志级别',
|
||||
options: [
|
||||
{ label: 'INFO', value: 'INFO' },
|
||||
{ label: 'WARN', value: 'WARN' },
|
||||
{ label: 'ERROR', value: 'ERROR' },
|
||||
{ label: 'DEBUG', value: 'DEBUG' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'module',
|
||||
label: '模块',
|
||||
type: 'select',
|
||||
placeholder: '请选择模块',
|
||||
options: [
|
||||
{ label: '用户管理', value: '用户管理' },
|
||||
{ label: '权限管理', value: '权限管理' },
|
||||
{ label: '系统配置', value: '系统配置' },
|
||||
{ label: '数据备份', value: '数据备份' },
|
||||
{ label: '登录认证', value: '登录认证' },
|
||||
],
|
||||
},
|
||||
{
|
||||
field: 'operator',
|
||||
label: '操作人',
|
||||
type: 'input',
|
||||
placeholder: '请输入操作人',
|
||||
},
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed<TableColumnData[]>(() => [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'index',
|
||||
slotName: 'index',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '日志级别',
|
||||
dataIndex: 'level',
|
||||
slotName: 'level',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'module',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '日志内容',
|
||||
dataIndex: 'content',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'operator',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'IP地址',
|
||||
dataIndex: 'ip',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '操作时间',
|
||||
dataIndex: 'createdAt',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'operations',
|
||||
slotName: 'operations',
|
||||
width: 100,
|
||||
fixed: 'right',
|
||||
},
|
||||
])
|
||||
|
||||
// 获取日志级别颜色
|
||||
const getLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
INFO: 'arcoblue',
|
||||
WARN: 'orange',
|
||||
ERROR: 'red',
|
||||
DEBUG: 'gray',
|
||||
}
|
||||
return colorMap[level] || 'gray'
|
||||
}
|
||||
|
||||
// 模拟异步获取数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
let data = generateMockData(100)
|
||||
|
||||
// 根据搜索条件过滤
|
||||
if (formModel.value.level) {
|
||||
data = data.filter(item => item.level === formModel.value.level)
|
||||
}
|
||||
if (formModel.value.module) {
|
||||
data = data.filter(item => item.module === formModel.value.module)
|
||||
}
|
||||
if (formModel.value.operator) {
|
||||
data = data.filter(item => item.operator.includes(formModel.value.operator))
|
||||
}
|
||||
|
||||
// 更新分页
|
||||
pagination.total = data.length
|
||||
|
||||
// 分页截取
|
||||
const start = (pagination.current - 1) * pagination.pageSize
|
||||
const end = start + pagination.pageSize
|
||||
tableData.value = data.slice(start, end)
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 事件处理
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
level: '',
|
||||
module: '',
|
||||
operator: '',
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchData()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
Message.info('导出功能开发中...')
|
||||
}
|
||||
|
||||
const handleView = (record: LogRecord) => {
|
||||
Message.info(`查看日志详情:${record.id}`)
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
fetchData()
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SystemLogs',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<a-card class="general-card" :title="$t('userInfo.title.latestActivity')">
|
||||
<template #extra>
|
||||
<a-link>{{ $t('userInfo.viewAll') }}</a-link>
|
||||
</template>
|
||||
<a-list :bordered="false">
|
||||
<a-list-item v-for="activity in activityList" :key="activity.id" action-layout="horizontal">
|
||||
<a-skeleton v-if="loading" :loading="loading" :animation="true" class="skeleton-item">
|
||||
<a-row :gutter="6">
|
||||
<a-col :span="2">
|
||||
<a-skeleton-shape shape="circle" />
|
||||
</a-col>
|
||||
<a-col :span="22">
|
||||
<a-skeleton-line :widths="['40%', '100%']" :rows="2" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-skeleton>
|
||||
<a-list-item-meta v-else :title="activity.title" :description="activity.description">
|
||||
<template #avatar>
|
||||
<a-avatar>
|
||||
<img :src="activity.avatar" />
|
||||
</a-avatar>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { queryLatestActivity, LatestActivity } from '@/api/user-center'
|
||||
import useLoading from '@/hooks/loading'
|
||||
|
||||
const { loading, setLoading } = useLoading(true)
|
||||
const activityList = ref<LatestActivity[]>(new Array(7).fill({}))
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { data } = await queryLatestActivity()
|
||||
activityList.value = data
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.latest-activity {
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
.general-card :deep(.arco-list-item) {
|
||||
padding-left: 0;
|
||||
border-bottom: none;
|
||||
.arco-list-item-meta-content {
|
||||
flex: 1;
|
||||
padding-bottom: 27px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
.arco-list-item-meta-avatar {
|
||||
padding-bottom: 27px;
|
||||
}
|
||||
.skeleton-item {
|
||||
margin-top: 10px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<a-card class="general-card" :title="$t('userInfo.title.latestNotification')">
|
||||
<a-skeleton v-if="loading" :animation="true">
|
||||
<a-skeleton-line :rows="3" />
|
||||
</a-skeleton>
|
||||
<a-result v-else status="404">
|
||||
<template #subtitle>
|
||||
{{ $t('userInfo.nodata') }}
|
||||
</template>
|
||||
</a-result>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useLoading from '@/hooks/loading'
|
||||
|
||||
const { loading, setLoading } = useLoading(true)
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
}, 500)
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-result) {
|
||||
padding: 40px 32px 108px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,82 +0,0 @@
|
||||
<template>
|
||||
<a-card class="general-card" :title="$t('userInfo.title.myProject')">
|
||||
<template #extra>
|
||||
<a-link>{{ $t('userInfo.showMore') }}</a-link>
|
||||
</template>
|
||||
<a-row :gutter="16">
|
||||
<a-col
|
||||
v-for="(project, index) in projectList"
|
||||
:key="index"
|
||||
:xs="12"
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="12"
|
||||
:xl="8"
|
||||
:xxl="8"
|
||||
class="my-project-item"
|
||||
>
|
||||
<a-card>
|
||||
<a-skeleton v-if="loading" :loading="loading" :animation="true">
|
||||
<a-skeleton-line :rows="3" />
|
||||
</a-skeleton>
|
||||
<a-space v-else direction="vertical">
|
||||
<a-typography-text bold>{{ project.name }}</a-typography-text>
|
||||
<a-typography-text type="secondary">
|
||||
{{ project.description }}
|
||||
</a-typography-text>
|
||||
<a-space>
|
||||
<a-avatar-group :size="24">
|
||||
{{ project.contributors }}
|
||||
<a-avatar v-for="(contributor, idx) in project.contributors" :key="idx" :size="32">
|
||||
<img alt="avatar" :src="contributor.avatar" />
|
||||
</a-avatar>
|
||||
</a-avatar-group>
|
||||
<a-typography-text type="secondary">等{{ project.peopleNumber }}人</a-typography-text>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { queryMyProjectList, MyProjectRecord } from '@/api/user-center'
|
||||
import useRequest from '@/hooks/request'
|
||||
|
||||
const defaultValue = Array(6).fill({} as MyProjectRecord)
|
||||
const { loading, response: projectList } = useRequest<MyProjectRecord[]>(queryMyProjectList, defaultValue)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-card-body) {
|
||||
min-height: 128px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.my-project {
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 18px !important;
|
||||
}
|
||||
|
||||
&-list {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-item {
|
||||
// padding-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,57 +0,0 @@
|
||||
<template>
|
||||
<a-card
|
||||
class="general-card"
|
||||
:title="$t('userInfo.tab.title.team')"
|
||||
:header-style="{ paddingBottom: '18px' }"
|
||||
:body-style="{ paddingBottom: '12px' }"
|
||||
>
|
||||
<a-list :bordered="false">
|
||||
<a-list-item v-for="team in teamList" :key="team.id" action-layout="horizontal">
|
||||
<a-skeleton v-if="loading" :loading="loading" :animation="true">
|
||||
<a-row :gutter="6">
|
||||
<a-col :span="6">
|
||||
<a-skeleton-shape shape="circle" />
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-skeleton-line :widths="['100%', '40%']" :rows="2" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-skeleton>
|
||||
<a-list-item-meta v-else :title="team.name">
|
||||
<template #avatar>
|
||||
<a-avatar>
|
||||
<img :src="team.avatar" />
|
||||
</a-avatar>
|
||||
</template>
|
||||
<template #description>共{{ team.peopleNumber }}人</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { queryMyTeamList, MyTeamRecord } from '@/api/user-center'
|
||||
import useRequest from '@/hooks/request'
|
||||
|
||||
const defaultValue: MyTeamRecord[] = new Array(4).fill({})
|
||||
const { loading, response: teamList } = useRequest<MyTeamRecord[]>(queryMyTeamList, defaultValue)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.general-card {
|
||||
height: 356px;
|
||||
.arco-list-item {
|
||||
height: 72px;
|
||||
padding-left: 0;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.arco-list-item-meta {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<a-space :size="12" direction="vertical" align="center">
|
||||
<a-avatar :size="64">
|
||||
<template #trigger-icon>
|
||||
<icon-camera />
|
||||
</template>
|
||||
<img :src="userInfo.avatar" />
|
||||
</a-avatar>
|
||||
<a-typography-title :heading="6" style="margin: 0">
|
||||
{{ userInfo.name }}
|
||||
</a-typography-title>
|
||||
<div class="user-msg">
|
||||
<a-space :size="18">
|
||||
<div>
|
||||
<icon-user />
|
||||
<a-typography-text>{{ userInfo.jobName }}</a-typography-text>
|
||||
</div>
|
||||
<div>
|
||||
<icon-home />
|
||||
<a-typography-text>
|
||||
{{ userInfo.organizationName }}
|
||||
</a-typography-text>
|
||||
</div>
|
||||
<div>
|
||||
<icon-location />
|
||||
<a-typography-text>{{ userInfo.locationName }}</a-typography-text>
|
||||
</div>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
const userInfo = useUserStore()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 204px;
|
||||
color: var(--gray-10);
|
||||
background: url(//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/41c6b125cc2e27021bf7fcc9a9b1897c.svg~tplv-49unhts6dw-image.image) no-repeat;
|
||||
background-size: cover;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.arco-avatar-trigger-icon-button) {
|
||||
color: rgb(var(--arcoblue-6));
|
||||
|
||||
:deep(.arco-icon) {
|
||||
vertical-align: -1px;
|
||||
}
|
||||
}
|
||||
.user-msg {
|
||||
.arco-icon {
|
||||
color: rgb(var(--gray-10));
|
||||
}
|
||||
.arco-typography {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.user', 'menu.user.info']" />
|
||||
<UserInfoHeader />
|
||||
<div class="content">
|
||||
<div class="content-left">
|
||||
<a-grid :cols="24" :col-gap="16" :row-gap="16">
|
||||
<a-grid-item :span="24">
|
||||
<MyProject />
|
||||
</a-grid-item>
|
||||
<a-grid-item :span="24">
|
||||
<LatestActivity />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
<div class="content-right">
|
||||
<a-grid :cols="24" :row-gap="16">
|
||||
<a-grid-item :span="24">
|
||||
<MyTeam />
|
||||
</a-grid-item>
|
||||
<a-grid-item class="panel" :span="24">
|
||||
<LatestNotification />
|
||||
</a-grid-item>
|
||||
</a-grid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UserInfoHeader from './components/user-info-header.vue'
|
||||
import LatestNotification from './components/latest-notification.vue'
|
||||
import MyProject from './components/my-project.vue'
|
||||
import LatestActivity from './components/latest-activity.vue'
|
||||
import MyTeam from './components/my-team.vue'
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Info',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
margin-top: 12px;
|
||||
|
||||
&-left {
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
// background-color: var(--color-bg-2);
|
||||
|
||||
:deep(.arco-tabs-nav-tab) {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
width: 332px;
|
||||
}
|
||||
|
||||
.tab-pane-wrapper {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mobile {
|
||||
.content {
|
||||
display: block;
|
||||
&-left {
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
&-right {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
'menu.user.info': 'User Info',
|
||||
'userInfo.editUserInfo': 'Edit Info',
|
||||
'userInfo.tab.title.overview': 'Overview',
|
||||
'userInfo.tab.title.project': 'Project',
|
||||
'userInfo.tab.title.team': 'My Team',
|
||||
'userInfo.title.latestActivity': 'Latest Activity',
|
||||
'userInfo.title.latestNotification': 'In-site Notification',
|
||||
'userInfo.title.myProject': 'My Project',
|
||||
'userInfo.showMore': 'Show More',
|
||||
'userInfo.viewAll': 'View All',
|
||||
'userInfo.nodata': 'No Data',
|
||||
'userInfo.visits.unit': 'times',
|
||||
'userInfo.visits.lastMonth': 'Last Month',
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
'menu.user.info': '用户信息',
|
||||
'userInfo.editUserInfo': '编辑信息',
|
||||
'userInfo.tab.title.overview': '总览',
|
||||
'userInfo.tab.title.project': '项目',
|
||||
'userInfo.tab.title.team': '我的团队',
|
||||
'userInfo.title.latestActivity': '最新动态',
|
||||
'userInfo.title.latestNotification': '站内通知',
|
||||
'userInfo.title.myProject': '我的项目',
|
||||
'userInfo.showMore': '查看更多',
|
||||
'userInfo.viewAll': '查看全部',
|
||||
'userInfo.nodata': '暂无数据',
|
||||
'userInfo.visits.unit': '人次',
|
||||
'userInfo.visits.lastMonth': '较上月',
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
import Mock from 'mockjs'
|
||||
import setupMock, { successResponseWrap } from '@/utils/setup-mock'
|
||||
|
||||
setupMock({
|
||||
setup() {
|
||||
// 最新项目
|
||||
Mock.mock(new RegExp('/api/user/my-project/list'), () => {
|
||||
const contributors = [
|
||||
{
|
||||
name: '秦臻宇',
|
||||
email: 'qingzhenyu@arco.design',
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp',
|
||||
},
|
||||
{
|
||||
name: '于涛',
|
||||
email: 'yuebao@arco.design',
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp',
|
||||
},
|
||||
{
|
||||
name: '宁波',
|
||||
email: 'ningbo@arco.design',
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
},
|
||||
{
|
||||
name: '郑曦月',
|
||||
email: 'zhengxiyue@arco.design',
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/8361eeb82904210b4f55fab888fe8416.png~tplv-uwbnlip3yd-webp.webp',
|
||||
},
|
||||
{
|
||||
name: '宁波',
|
||||
email: 'ningbo@arco.design',
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
},
|
||||
]
|
||||
const units = [
|
||||
{
|
||||
name: '企业级产品设计系统',
|
||||
description: 'System',
|
||||
},
|
||||
{
|
||||
name: '智能应用',
|
||||
description: 'The Volcano Engine',
|
||||
},
|
||||
{
|
||||
name: 'OCR文本识别',
|
||||
description: 'OCR text recognition',
|
||||
},
|
||||
{
|
||||
name: '内容资源管理',
|
||||
description: 'Content resource management ',
|
||||
},
|
||||
{
|
||||
name: '今日头条内容管理',
|
||||
description: 'Toutiao content management',
|
||||
},
|
||||
{
|
||||
name: '智能机器人',
|
||||
description: 'Intelligent Robot Project',
|
||||
},
|
||||
]
|
||||
return successResponseWrap(
|
||||
new Array(6).fill(null).map((_item, index) => ({
|
||||
id: index,
|
||||
name: units[index].name,
|
||||
description: units[index].description,
|
||||
peopleNumber: Mock.Random.natural(10, 1000),
|
||||
contributors,
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
// 最新动态
|
||||
Mock.mock(new RegExp('/api/user/latest-activity'), () => {
|
||||
return successResponseWrap(
|
||||
new Array(7).fill(null).map((_item, index) => ({
|
||||
id: index,
|
||||
title: '发布了项目',
|
||||
description: '企业级产品设计系统',
|
||||
avatar: '//lf1-xgcdn-tos.pstatp.com/obj/vcloud/vadmin/start.8e0e4855ee346a46ccff8ff3e24db27b.png',
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
// 访问量
|
||||
Mock.mock(new RegExp('/api/user/visits'), () => {
|
||||
return successResponseWrap([
|
||||
{
|
||||
name: '主页访问量',
|
||||
visits: 5670,
|
||||
growth: 206.32,
|
||||
},
|
||||
{
|
||||
name: '项目访问量',
|
||||
visits: 5670,
|
||||
growth: 206.32,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
// 项目和团队列表
|
||||
Mock.mock(new RegExp('/api/user/project-and-team/list'), () => {
|
||||
return successResponseWrap([
|
||||
{
|
||||
id: 1,
|
||||
content: '他创建的项目',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: '他参与的项目',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: '他创建的团队',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
content: '他加入的团队',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
// 团队列表
|
||||
Mock.mock(new RegExp('/api/user/my-team/list'), () => {
|
||||
return successResponseWrap([
|
||||
{
|
||||
id: 1,
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/a8c8cdb109cb051163646151a4a5083b.png~tplv-uwbnlip3yd-webp.webp',
|
||||
name: '火智能应用团队',
|
||||
peopleNumber: Mock.Random.natural(10, 100),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
name: '企业级产品设计团队',
|
||||
peopleNumber: Mock.Random.natural(5000, 6000),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
name: '前端/UE小分队',
|
||||
peopleNumber: Mock.Random.natural(10, 5000),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
avatar: '//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/8361eeb82904210b4f55fab888fe8416.png~tplv-uwbnlip3yd-webp.webp',
|
||||
name: '内容识别插件小分队',
|
||||
peopleNumber: Mock.Random.natural(10, 100),
|
||||
},
|
||||
])
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,135 +0,0 @@
|
||||
<template>
|
||||
<a-form ref="formRef" :model="formData" class="form" :label-col-props="{ span: 8 }" :wrapper-col-props="{ span: 16 }">
|
||||
<a-form-item
|
||||
field="email"
|
||||
:label="$t('userSetting.basicInfo.form.label.email')"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: $t('userSetting.form.error.email.required'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input v-model="formData.email" :placeholder="$t('userSetting.basicInfo.placeholder.email')" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="nickname"
|
||||
:label="$t('userSetting.basicInfo.form.label.nickname')"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: $t('userSetting.form.error.nickname.required'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-input v-model="formData.nickname" :placeholder="$t('userSetting.basicInfo.placeholder.nickname')" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="countryRegion"
|
||||
:label="$t('userSetting.basicInfo.form.label.countryRegion')"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: $t('userSetting.form.error.countryRegion.required'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-select v-model="formData.countryRegion" :placeholder="$t('userSetting.basicInfo.placeholder.area')">
|
||||
<a-option value="China">中国</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="area"
|
||||
:label="$t('userSetting.basicInfo.form.label.area')"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: $t('userSetting.form.error.area.required'),
|
||||
},
|
||||
]"
|
||||
>
|
||||
<a-cascader
|
||||
v-model="formData.area"
|
||||
:placeholder="$t('userSetting.basicInfo.placeholder.area')"
|
||||
:options="[
|
||||
{
|
||||
label: '北京',
|
||||
value: 'beijing',
|
||||
children: [
|
||||
{
|
||||
label: '北京',
|
||||
value: 'beijing',
|
||||
children: [
|
||||
{
|
||||
label: '朝阳',
|
||||
value: 'chaoyang',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item field="address" :label="$t('userSetting.basicInfo.form.label.address')">
|
||||
<a-input v-model="formData.address" :placeholder="$t('userSetting.basicInfo.placeholder.address')" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
field="profile"
|
||||
:label="$t('userSetting.basicInfo.form.label.profile')"
|
||||
:rules="[
|
||||
{
|
||||
maxLength: 200,
|
||||
message: $t('userSetting.form.error.profile.maxLength'),
|
||||
},
|
||||
]"
|
||||
row-class="keep-margin"
|
||||
>
|
||||
<a-textarea v-model="formData.profile" :placeholder="$t('userSetting.basicInfo.placeholder.profile')" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="validate">
|
||||
{{ $t('userSetting.save') }}
|
||||
</a-button>
|
||||
<a-button type="secondary" @click="reset">
|
||||
{{ $t('userSetting.reset') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { FormInstance } from '@arco-design/web-vue/es/form'
|
||||
import { BasicInfoModel } from '@/api/user-center'
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const formData = ref<BasicInfoModel>({
|
||||
email: '',
|
||||
nickname: '',
|
||||
countryRegion: '',
|
||||
area: '',
|
||||
address: '',
|
||||
profile: '',
|
||||
})
|
||||
const validate = async () => {
|
||||
const res = await formRef.value?.validate()
|
||||
if (!res) {
|
||||
// do some thing
|
||||
// you also can use html-type to submit
|
||||
}
|
||||
}
|
||||
const reset = async () => {
|
||||
await formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form {
|
||||
width: 540px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +0,0 @@
|
||||
<template>
|
||||
<a-card class="general-card" :title="$t('userSetting.certification.title.record')" :header-style="{ border: 'none' }">
|
||||
<a-table v-if="renderData.length" :data="renderData">
|
||||
<template #columns>
|
||||
<a-table-column :title="$t('userSetting.certification.columns.certificationType')">
|
||||
<template #cell>
|
||||
{{ $t('userSetting.certification.cell.certificationType') }}
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column :title="$t('userSetting.certification.columns.certificationContent')" data-index="certificationContent" />
|
||||
<a-table-column :title="$t('userSetting.certification.columns.status')">
|
||||
<template #cell="{ record }">
|
||||
<p v-if="record.status === 0">
|
||||
<span class="circle"></span>
|
||||
<span>{{ $t('userSetting.certification.cell.auditing') }}</span>
|
||||
</p>
|
||||
<p v-if="record.status === 1">
|
||||
<span class="circle pass"></span>
|
||||
<span>{{ $t('userSetting.certification.cell.pass') }}</span>
|
||||
</p>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column :title="$t('userSetting.certification.columns.time')" data-index="time" />
|
||||
<a-table-column :title="$t('userSetting.certification.columns.operation')">
|
||||
<template #cell="{ record }">
|
||||
<a-space>
|
||||
<a-button type="text">
|
||||
{{ $t('userSetting.certification.button.check') }}
|
||||
</a-button>
|
||||
<a-button v-if="record.status === 0" type="text">
|
||||
{{ $t('userSetting.certification.button.withdraw') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue'
|
||||
import { CertificationRecord } from '@/api/user-center'
|
||||
|
||||
defineProps({
|
||||
renderData: {
|
||||
type: Array as PropType<CertificationRecord>,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-table-th) {
|
||||
&:last-child {
|
||||
.arco-table-th-item-title {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,33 +0,0 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<EnterpriseCertification :enterprise-info="data.enterpriseInfo" />
|
||||
<CertificationRecords :render-data="data.record" />
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { queryCertification, UnitCertification, EnterpriseCertificationModel } from '@/api/user-center'
|
||||
import useLoading from '@/hooks/loading'
|
||||
import EnterpriseCertification from './enterprise-certification.vue'
|
||||
import CertificationRecords from './certification-records.vue'
|
||||
|
||||
const { loading, setLoading } = useLoading(true)
|
||||
const data = ref<UnitCertification>({
|
||||
enterpriseInfo: {} as EnterpriseCertificationModel,
|
||||
record: [],
|
||||
})
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { data: resData } = await queryCertification()
|
||||
data.value = resData
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<a-card class="general-card" :title="$t('userSetting.certification.title.enterprise')" :header-style="{ padding: '0px 20px 16px 20px' }">
|
||||
<template #extra>
|
||||
<a-link>{{ $t('userSetting.certification.extra.enterprise') }}</a-link>
|
||||
</template>
|
||||
<a-descriptions
|
||||
class="card-content"
|
||||
:data="renderData"
|
||||
:column="3"
|
||||
align="right"
|
||||
layout="inline-horizontal"
|
||||
:label-style="{ fontWeight: 'normal' }"
|
||||
:value-style="{
|
||||
width: '200px',
|
||||
paddingLeft: '8px',
|
||||
textAlign: 'left',
|
||||
}"
|
||||
>
|
||||
<template #label="{ label }">{{ $t(label) }} :</template>
|
||||
<template #value="{ value, data }">
|
||||
<a-tag v-if="data.label === 'userSetting.certification.label.status'" color="green" size="small">已认证</a-tag>
|
||||
<span v-else>{{ value }}</span>
|
||||
</template>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType, computed } from 'vue'
|
||||
import { EnterpriseCertificationModel } from '@/api/user-center'
|
||||
import type { DescData } from '@arco-design/web-vue/es/descriptions/interface'
|
||||
|
||||
const props = defineProps({
|
||||
enterpriseInfo: {
|
||||
type: Object as PropType<EnterpriseCertificationModel>,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const renderData = computed(() => {
|
||||
const {
|
||||
accountType,
|
||||
status,
|
||||
time,
|
||||
legalPerson,
|
||||
certificateType,
|
||||
authenticationNumber,
|
||||
enterpriseName,
|
||||
enterpriseCertificateType,
|
||||
organizationCode,
|
||||
} = props.enterpriseInfo
|
||||
return [
|
||||
{
|
||||
label: 'userSetting.certification.label.accountType',
|
||||
value: accountType,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.certification.label.status',
|
||||
value: status,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.certification.label.time',
|
||||
value: time,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.certification.label.legalPerson',
|
||||
value: legalPerson,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.certification.label.certificateType',
|
||||
value: certificateType,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.certification.label.authenticationNumber',
|
||||
value: authenticationNumber,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.certification.label.enterpriseName',
|
||||
value: enterpriseName,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.certification.label.enterpriseCertificateType',
|
||||
value: enterpriseCertificateType,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.certification.label.organizationCode',
|
||||
value: organizationCode,
|
||||
},
|
||||
] as DescData[]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.card-content {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background-color: rgb(var(--gray-1));
|
||||
}
|
||||
.item-label {
|
||||
min-width: 98px;
|
||||
text-align: right;
|
||||
color: var(--color-text-8);
|
||||
&:after {
|
||||
content: ':';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,120 +0,0 @@
|
||||
<template>
|
||||
<a-list :bordered="false">
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-typography-paragraph>
|
||||
{{ $t('userSetting.SecuritySettings.form.label.password') }}
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="content">
|
||||
<a-typography-paragraph>
|
||||
{{ $t('userSetting.SecuritySettings.placeholder.password') }}
|
||||
</a-typography-paragraph>
|
||||
</div>
|
||||
<div class="operation">
|
||||
<a-link>
|
||||
{{ $t('userSetting.SecuritySettings.button.update') }}
|
||||
</a-link>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-typography-paragraph>
|
||||
{{ $t('userSetting.SecuritySettings.form.label.securityQuestion') }}
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="content">
|
||||
<a-typography-paragraph class="tip">
|
||||
{{ $t('userSetting.SecuritySettings.placeholder.securityQuestion') }}
|
||||
</a-typography-paragraph>
|
||||
</div>
|
||||
<div class="operation">
|
||||
<a-link>
|
||||
{{ $t('userSetting.SecuritySettings.button.settings') }}
|
||||
</a-link>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-typography-paragraph>
|
||||
{{ $t('userSetting.SecuritySettings.form.label.phone') }}
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="content">
|
||||
<a-typography-paragraph>已绑定:150******50</a-typography-paragraph>
|
||||
</div>
|
||||
<div class="operation">
|
||||
<a-link>
|
||||
{{ $t('userSetting.SecuritySettings.button.update') }}
|
||||
</a-link>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-list-item-meta>
|
||||
<template #avatar>
|
||||
<a-typography-paragraph>
|
||||
{{ $t('userSetting.SecuritySettings.form.label.email') }}
|
||||
</a-typography-paragraph>
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="content">
|
||||
<a-typography-paragraph class="tip">
|
||||
{{ $t('userSetting.SecuritySettings.placeholder.email') }}
|
||||
</a-typography-paragraph>
|
||||
</div>
|
||||
<div class="operation">
|
||||
<a-link>
|
||||
{{ $t('userSetting.SecuritySettings.button.update') }}
|
||||
</a-link>
|
||||
</div>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style scoped lang="less">
|
||||
:deep(.arco-list-item) {
|
||||
border-bottom: none !important;
|
||||
.arco-typography {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.arco-list-item-meta-avatar {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
.arco-list-item-meta {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
:deep(.arco-list-item-meta-content) {
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--color-neutral-3);
|
||||
|
||||
.arco-list-item-meta-description {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
|
||||
.tip {
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
.operation {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,140 +0,0 @@
|
||||
<template>
|
||||
<a-card :bordered="false">
|
||||
<a-space :size="54">
|
||||
<a-upload
|
||||
:custom-request="customRequest"
|
||||
list-type="picture-card"
|
||||
:file-list="fileList"
|
||||
:show-upload-button="true"
|
||||
:show-file-list="false"
|
||||
@change="uploadChange"
|
||||
>
|
||||
<template #upload-button>
|
||||
<a-avatar :size="100" class="info-avatar">
|
||||
<template #trigger-icon>
|
||||
<icon-camera />
|
||||
</template>
|
||||
<img v-if="fileList.length" :src="fileList[0].url" />
|
||||
</a-avatar>
|
||||
</template>
|
||||
</a-upload>
|
||||
<a-descriptions
|
||||
:data="renderData"
|
||||
:column="2"
|
||||
align="right"
|
||||
layout="inline-horizontal"
|
||||
:label-style="{
|
||||
width: '140px',
|
||||
fontWeight: 'normal',
|
||||
color: 'rgb(var(--gray-8))',
|
||||
}"
|
||||
:value-style="{
|
||||
width: '200px',
|
||||
paddingLeft: '8px',
|
||||
textAlign: 'left',
|
||||
}"
|
||||
>
|
||||
<template #label="{ label }">{{ $t(label) }} :</template>
|
||||
<template #value="{ value, data }">
|
||||
<a-tag v-if="data.label === 'userSetting.label.certification'" color="green" size="small">已认证</a-tag>
|
||||
<span v-else>{{ value }}</span>
|
||||
</template>
|
||||
</a-descriptions>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import type { FileItem, RequestOption } from '@arco-design/web-vue/es/upload/interfaces'
|
||||
import { useUserStore } from '@/store'
|
||||
import { userUploadApi } from '@/api/user-center'
|
||||
import type { DescData } from '@arco-design/web-vue/es/descriptions/interface'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const file = {
|
||||
uid: '-2',
|
||||
name: 'avatar.png',
|
||||
url: userStore.avatar,
|
||||
}
|
||||
const renderData = [
|
||||
{
|
||||
label: 'userSetting.label.name',
|
||||
value: userStore.name,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.label.certification',
|
||||
value: userStore.certification,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.label.accountId',
|
||||
value: userStore.accountId,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.label.phone',
|
||||
value: userStore.phone,
|
||||
},
|
||||
{
|
||||
label: 'userSetting.label.registrationDate',
|
||||
value: userStore.registrationDate,
|
||||
},
|
||||
] as DescData[]
|
||||
const fileList = ref<FileItem[]>([file])
|
||||
const uploadChange = (fileItemList: FileItem[], fileItem: FileItem) => {
|
||||
fileList.value = [fileItem]
|
||||
}
|
||||
const customRequest = (options: RequestOption) => {
|
||||
// docs: https://axios-http.com/docs/cancellation
|
||||
const controller = new AbortController()
|
||||
|
||||
;(async function requestWrap() {
|
||||
const { onProgress, onError, onSuccess, fileItem, name = 'file' } = options
|
||||
onProgress(20)
|
||||
const formData = new FormData()
|
||||
formData.append(name as string, fileItem.file as Blob)
|
||||
const onUploadProgress = (event: ProgressEvent) => {
|
||||
let percent
|
||||
if (event.total > 0) {
|
||||
percent = (event.loaded / event.total) * 100
|
||||
}
|
||||
onProgress(parseInt(String(percent), 10), event)
|
||||
}
|
||||
|
||||
try {
|
||||
// https://github.com/axios/axios/issues/1630
|
||||
// https://github.com/nuysoft/Mock/issues/127
|
||||
|
||||
const res = await userUploadApi(formData, {
|
||||
controller,
|
||||
onUploadProgress,
|
||||
})
|
||||
onSuccess(res)
|
||||
} catch (error) {
|
||||
onError(error)
|
||||
}
|
||||
})()
|
||||
return {
|
||||
abort() {
|
||||
controller.abort()
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.arco-card {
|
||||
padding: 14px 0 4px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
:deep(.arco-avatar-trigger-icon-button) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
background-color: #e8f3ff;
|
||||
.arco-icon-camera {
|
||||
margin-top: 8px;
|
||||
color: rgb(var(--arcoblue-6));
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,57 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<Breadcrumb :items="['menu.user', 'menu.user.setting']" />
|
||||
<a-row style="margin-bottom: 16px">
|
||||
<a-col :span="24">
|
||||
<UserPanel />
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row class="wrapper">
|
||||
<a-col :span="24">
|
||||
<a-tabs default-active-key="1" type="rounded">
|
||||
<a-tab-pane key="1" :title="$t('userSetting.tab.basicInformation')">
|
||||
<BasicInformation />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" :title="$t('userSetting.tab.securitySettings')">
|
||||
<SecuritySettings />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" :title="$t('userSetting.tab.certification')">
|
||||
<Certification />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UserPanel from './components/user-panel.vue'
|
||||
import BasicInformation from './components/basic-information.vue'
|
||||
import SecuritySettings from './components/security-settings.vue'
|
||||
import Certification from './components/certification.vue'
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Setting',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: 20px 0 0 20px;
|
||||
min-height: 580px;
|
||||
background-color: var(--color-bg-2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.section-title) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,80 +0,0 @@
|
||||
export default {
|
||||
'menu.user.setting': 'User Setting',
|
||||
'userSetting.menu.title.info': 'Personal Information',
|
||||
'userSetting.menu.title.account': 'Account Setting',
|
||||
'userSetting.menu.title.password': 'Password',
|
||||
'userSetting.menu.title.message': 'Message Notification',
|
||||
'userSetting.menu.title.result': 'Result',
|
||||
'userSetting.menu.title.data': 'Export Data',
|
||||
'userSetting.saveSuccess': 'Save Success',
|
||||
'userSetting.title.basicInfo': 'Basic Information',
|
||||
'userSetting.title.socialInfo': 'Social Information',
|
||||
'userSetting.label.avatar': 'Avatar',
|
||||
'userSetting.label.name': 'User Name',
|
||||
'userSetting.label.location': 'Office Location',
|
||||
'userSetting.label.introduction': 'Introduction',
|
||||
'userSetting.label.personalWebsite': 'Website',
|
||||
'userSetting.save': 'Save',
|
||||
'userSetting.cancel': 'Cancel',
|
||||
'userSetting.reset': 'Reset',
|
||||
// new
|
||||
'userSetting.label.certification': 'Certification',
|
||||
'userSetting.label.phone': 'Phone',
|
||||
'userSetting.label.accountId': 'Account Id',
|
||||
'userSetting.label.registrationDate': 'Registration Date',
|
||||
'userSetting.tab.basicInformation': 'Basic Information',
|
||||
'userSetting.tab.securitySettings': 'Security Settings',
|
||||
'userSetting.tab.certification': 'Certification',
|
||||
'userSetting.basicInfo.form.label.email': 'Email',
|
||||
'userSetting.basicInfo.placeholder.email': `Please enter your email address, such as xxx{'@'}bytedance.com`,
|
||||
'userSetting.form.error.email.required': 'Please enter email address',
|
||||
'userSetting.basicInfo.form.label.nickname': 'Nickname',
|
||||
'userSetting.basicInfo.placeholder.nickname': 'Please enter nickname',
|
||||
'userSetting.form.error.nickname.required': 'Please enter nickname',
|
||||
'userSetting.basicInfo.form.label.countryRegion': 'Country/region',
|
||||
'userSetting.basicInfo.placeholder.countryRegion': 'Please select country/region',
|
||||
'userSetting.form.error.countryRegion.required': 'Please select country/region',
|
||||
'userSetting.basicInfo.form.label.area': 'Area',
|
||||
'userSetting.basicInfo.placeholder.area': 'Please select area',
|
||||
'userSetting.form.error.area.required': 'Please Select a area',
|
||||
'userSetting.basicInfo.form.label.address': 'Address',
|
||||
'userSetting.basicInfo.placeholder.address': 'Please enter address',
|
||||
'userSetting.basicInfo.form.label.profile': 'Personal profile',
|
||||
'userSetting.basicInfo.placeholder.profile': 'Please enter your profile, no more than 200 words',
|
||||
'userSetting.form.error.profile.maxLength': 'No more than 200 words',
|
||||
'userSetting.SecuritySettings.form.label.password': 'Login Password',
|
||||
'userSetting.SecuritySettings.placeholder.password':
|
||||
'Has been set. The password must contain at least six letters, digits, and special characters except Spaces. The password must contain both uppercase and lowercase letters.',
|
||||
'userSetting.SecuritySettings.form.label.securityQuestion': 'Security Question',
|
||||
'userSetting.SecuritySettings.placeholder.securityQuestion':
|
||||
'You have not set the password protection question. The password protection question can effectively protect the account security.',
|
||||
'userSetting.SecuritySettings.form.label.phone': 'Phone',
|
||||
// 'userSetting.SecuritySettings.placeholder.phone': '已绑定:150******50',
|
||||
'userSetting.SecuritySettings.form.label.email': 'Email',
|
||||
'userSetting.SecuritySettings.placeholder.email':
|
||||
'You have not set a mailbox yet. The mailbox binding can be used to retrieve passwords and receive notifications.',
|
||||
'userSetting.SecuritySettings.button.settings': 'Settings',
|
||||
'userSetting.SecuritySettings.button.update': 'Update',
|
||||
'userSetting.certification.title.enterprise': 'Enterprise Real Name Authentication',
|
||||
'userSetting.certification.extra.enterprise': 'Modifying an Authentication Body',
|
||||
'userSetting.certification.label.accountType': 'Account Type',
|
||||
'userSetting.certification.label.status': 'status',
|
||||
'userSetting.certification.label.time': 'time',
|
||||
'userSetting.certification.label.legalPerson': 'Legal Person Name',
|
||||
'userSetting.certification.label.certificateType': 'Types of legal person documents',
|
||||
'userSetting.certification.label.authenticationNumber': 'Legal person certification number',
|
||||
'userSetting.certification.label.enterpriseName': 'Enterprise Name',
|
||||
'userSetting.certification.label.enterpriseCertificateType': 'Types of corporate certificates',
|
||||
'userSetting.certification.label.organizationCode': 'Organization Code',
|
||||
'userSetting.certification.title.record': 'Certification Records',
|
||||
'userSetting.certification.columns.certificationType': 'Certification Type',
|
||||
'userSetting.certification.cell.certificationType': 'Enterprise certificate Certification',
|
||||
'userSetting.certification.columns.certificationContent': 'Certification Content',
|
||||
'userSetting.certification.columns.status': 'Status',
|
||||
'userSetting.certification.cell.pass': 'Pass',
|
||||
'userSetting.certification.cell.auditing': 'Auditing',
|
||||
'userSetting.certification.columns.time': 'Time',
|
||||
'userSetting.certification.columns.operation': 'Operation',
|
||||
'userSetting.certification.button.check': 'Check',
|
||||
'userSetting.certification.button.withdraw': 'Withdraw',
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
export default {
|
||||
'menu.user.setting': '用户设置',
|
||||
'userSetting.menu.title.info': '个人信息',
|
||||
'userSetting.menu.title.account': '账号设置',
|
||||
'userSetting.menu.title.password': '密码',
|
||||
'userSetting.menu.title.message': '消息通知',
|
||||
'userSetting.menu.title.result': '结果页',
|
||||
'userSetting.menu.title.data': '导出数据',
|
||||
'userSetting.saveSuccess': '保存成功',
|
||||
'userSetting.title.basicInfo': '基本信息',
|
||||
'userSetting.title.socialInfo': '社交信息',
|
||||
'userSetting.label.avatar': '头像',
|
||||
'userSetting.label.name': '用户名',
|
||||
'userSetting.label.location': '办公地点',
|
||||
'userSetting.label.introduction': '个人简介',
|
||||
'userSetting.label.personalWebsite': '个人网站',
|
||||
'userSetting.save': '保存',
|
||||
'userSetting.cancel': '取消',
|
||||
'userSetting.reset': '重置',
|
||||
// new
|
||||
'userSetting.label.certification': '实名认证',
|
||||
'userSetting.label.phone': '手机号码',
|
||||
'userSetting.label.accountId': '账号ID',
|
||||
'userSetting.label.registrationDate': '注册时间',
|
||||
'userSetting.tab.basicInformation': '基础信息',
|
||||
'userSetting.tab.securitySettings': '安全设置',
|
||||
'userSetting.tab.certification': '实名认证',
|
||||
'userSetting.basicInfo.form.label.email': '邮箱',
|
||||
'userSetting.basicInfo.placeholder.email': `请输入邮箱地址,如xxx{'@'}bytedance.com`,
|
||||
'userSetting.form.error.email.required': '请输入邮箱',
|
||||
'userSetting.basicInfo.form.label.nickname': '昵称',
|
||||
'userSetting.basicInfo.placeholder.nickname': '请输入您的昵称',
|
||||
'userSetting.form.error.nickname.required': '请输入昵称',
|
||||
'userSetting.basicInfo.form.label.countryRegion': '国家/地区',
|
||||
'userSetting.basicInfo.placeholder.countryRegion': '请选择',
|
||||
'userSetting.form.error.countryRegion.required': '请选择国家/地区',
|
||||
'userSetting.basicInfo.form.label.area': '所在区域',
|
||||
'userSetting.basicInfo.placeholder.area': '请选择',
|
||||
'userSetting.form.error.area.required': '请选择所在区域',
|
||||
'userSetting.basicInfo.form.label.address': '具体地址',
|
||||
'userSetting.basicInfo.placeholder.address': '请输入您的地址',
|
||||
'userSetting.basicInfo.form.label.profile': '个人简介',
|
||||
'userSetting.basicInfo.placeholder.profile': '请输入您的个人简介,最多不超过200字。',
|
||||
'userSetting.form.error.profile.maxLength': '最多不超过200字',
|
||||
'userSetting.SecuritySettings.form.label.password': '登录密码',
|
||||
'userSetting.SecuritySettings.placeholder.password':
|
||||
'已设置。密码至少6位字符,支持数字、字母和除空格外的特殊字符,且必须同时包含数字和大小写字母。',
|
||||
'userSetting.SecuritySettings.form.label.securityQuestion': '密保问题',
|
||||
'userSetting.SecuritySettings.placeholder.securityQuestion': '您暂未设置密保问题,密保问题可以有效的保护账号的安全。',
|
||||
'userSetting.SecuritySettings.form.label.phone': '安全手机',
|
||||
// 'userSetting.SecuritySettings.placeholder.phone': '已绑定:150******50',
|
||||
'userSetting.SecuritySettings.form.label.email': '安全邮箱',
|
||||
'userSetting.SecuritySettings.placeholder.email': '您暂未设置邮箱,绑定邮箱可以用来找回密码、接收通知等。',
|
||||
'userSetting.SecuritySettings.button.settings': '设置',
|
||||
'userSetting.SecuritySettings.button.update': '修改',
|
||||
'userSetting.certification.title.enterprise': '企业实名认证',
|
||||
'userSetting.certification.extra.enterprise': '修改认证主体',
|
||||
'userSetting.certification.label.accountType': '账号类型',
|
||||
'userSetting.certification.label.status': '认证状态',
|
||||
'userSetting.certification.label.time': '认证时间',
|
||||
'userSetting.certification.label.legalPerson': '法人姓名',
|
||||
'userSetting.certification.label.certificateType': '法人证件类型',
|
||||
'userSetting.certification.label.authenticationNumber': '法人认证号码',
|
||||
'userSetting.certification.label.enterpriseName': '企业名称',
|
||||
'userSetting.certification.label.enterpriseCertificateType': '企业证件类型',
|
||||
'userSetting.certification.label.organizationCode': '组织机构代码',
|
||||
'userSetting.certification.title.record': '认证记录',
|
||||
'userSetting.certification.columns.certificationType': '认证类型',
|
||||
'userSetting.certification.cell.certificationType': '企业证件认证',
|
||||
'userSetting.certification.columns.certificationContent': '认证内容',
|
||||
'userSetting.certification.columns.status': '当前状态',
|
||||
'userSetting.certification.cell.pass': '已通过',
|
||||
'userSetting.certification.cell.auditing': '审核中',
|
||||
'userSetting.certification.columns.time': '创建时间',
|
||||
'userSetting.certification.columns.operation': '操作',
|
||||
'userSetting.certification.button.check': '查看',
|
||||
'userSetting.certification.button.withdraw': '撤回',
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import Mock from 'mockjs'
|
||||
import setupMock, { successResponseWrap } from '@/utils/setup-mock'
|
||||
|
||||
setupMock({
|
||||
setup() {
|
||||
Mock.mock(new RegExp('/api/user/save-info'), () => {
|
||||
return successResponseWrap('ok')
|
||||
})
|
||||
Mock.mock(new RegExp('/api/user/certification'), () => {
|
||||
return successResponseWrap({
|
||||
enterpriseInfo: {
|
||||
accountType: '企业账号',
|
||||
status: 0,
|
||||
time: '2018-10-22 14:53:12',
|
||||
legalPerson: '李**',
|
||||
certificateType: '中国身份证',
|
||||
authenticationNumber: '130************123',
|
||||
enterpriseName: '低调有实力的企业',
|
||||
enterpriseCertificateType: '企业营业执照',
|
||||
organizationCode: '7*******9',
|
||||
},
|
||||
record: [
|
||||
{
|
||||
certificationType: 1,
|
||||
certificationContent: '企业实名认证,法人姓名:李**',
|
||||
status: 0,
|
||||
time: '2021-02-28 10:30:50',
|
||||
},
|
||||
{
|
||||
certificationType: 1,
|
||||
certificationContent: '企业实名认证,法人姓名:李**',
|
||||
status: 1,
|
||||
time: '2020-05-13 08:00:00',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
Mock.mock(new RegExp('/api/user/upload'), () => {
|
||||
return successResponseWrap('ok')
|
||||
})
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user