feat: complete rsf-design phase 1 integration
| | |
| | | # 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/) |
| | | VITE_BASE_URL = / |
| | | |
| | | # API 请求基础路径(开发环境设置为 / 使用代理,生产环境设置为完整后端地址) |
| | | VITE_API_URL = / |
| | | # API 请求基础路径(开发环境通过 rsf-server 上下文路径走代理) |
| | | VITE_API_URL = /rsf-server |
| | | |
| | | # 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题) |
| | | VITE_API_PROXY_URL = http://127.0.0.1:8085/ref-server |
| | | VITE_API_PROXY_URL = http://127.0.0.1:8085 |
| | | |
| | | # Delete console |
| | | VITE_DROP_CONSOLE = false |
| | |
| | | VITE_BASE_URL = / |
| | | |
| | | # API 地址前缀 |
| | | VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default |
| | | VITE_API_URL = /rsf-server |
| | | |
| | | # Delete console |
| | | VITE_DROP_CONSOLE = true |
| | |
| | | "pnpm": ">=8.8.0" |
| | | }, |
| | | "scripts": { |
| | | "test": "node --test tests/auth-contract.test.mjs tests/backend-menu-adapter.test.mjs tests/clean-dev-helpers.test.mjs tests/iconify-local-minimal.test.mjs tests/iconify-local-prefixes.test.mjs tests/manual-chunks.test.mjs tests/repo-hygiene.test.mjs tests/system-manage-contract.test.mjs tests/system-role-scope-contract.test.mjs tests/system-user-page-contract.test.mjs tests/work-tab-icon-contract.test.mjs tests/worktab-icon-normalization-contract.test.mjs", |
| | | "dev": "vite --open", |
| | | "build": "vite build", |
| | | "serve": "vite preview", |
| | |
| | | } |
| | | |
| | | function collectUsedIconsByPrefix() { |
| | | const iconPattern = /icon\s*[:=]\s*["']([a-z0-9-]+):([a-z0-9-]+)["']/g |
| | | const iconPattern = /["']([a-z0-9-]+):([a-z0-9-]+)["']/g |
| | | const knownPrefixes = new Set(Object.keys(iconCollections)) |
| | | const usedIconsByPrefix = new Map() |
| | | |
| | | for (const filePath of collectSourceFiles(srcRoot)) { |
| | | const content = fs.readFileSync(filePath, 'utf8') |
| | | |
| | | for (const [, prefix, name] of content.matchAll(iconPattern)) { |
| | | if (!knownPrefixes.has(prefix)) { |
| | | continue |
| | | } |
| | | |
| | | const names = usedIconsByPrefix.get(prefix) || new Set() |
| | | names.add(name) |
| | | usedIconsByPrefix.set(prefix, names) |
| | |
| | | return { |
| | | current: params.current || 1, |
| | | pageSize: params.pageSize || params.size || 20, |
| | | username: params.username, |
| | | nickname: params.nickname, |
| | | phone: params.phone, |
| | | status: params.status, |
| | | deptId: params.deptId |
| | | ...(params.username !== undefined ? { username: params.username } : {}), |
| | | ...(params.nickname !== undefined ? { nickname: params.nickname } : {}), |
| | | ...(params.phone !== undefined ? { phone: params.phone } : {}), |
| | | ...(params.email !== undefined ? { email: params.email } : {}), |
| | | ...(params.status !== undefined ? { status: params.status } : {}), |
| | | ...(params.deptId !== undefined ? { deptId: params.deptId } : {}) |
| | | } |
| | | } |
| | | |
| | |
| | | return { |
| | | current: params.current || 1, |
| | | pageSize: params.pageSize || params.size || 20, |
| | | name: params.name, |
| | | code: params.code, |
| | | memo: params.memo, |
| | | status: params.status |
| | | ...(params.name !== undefined ? { name: params.name } : {}), |
| | | ...(params.code !== undefined ? { code: params.code } : {}), |
| | | ...(params.memo !== undefined ? { memo: params.memo } : {}), |
| | | ...(params.status !== undefined ? { status: params.status } : {}) |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | function fetchResetUserPassword(params) { |
| | | return request.post({ url: '/auth/reset/password', params }) |
| | | const normalizedParams = normalizeAdminPasswordUpdateParams(params) |
| | | return request.post({ url: '/user/update', params: normalizedParams }) |
| | | } |
| | | |
| | | function fetchUpdateUserStatus(params) { |
| | |
| | | return request.post({ url: '/role/list', params }) |
| | | } |
| | | |
| | | function buildRolePageParams(params = {}) { |
| | | return { |
| | | current: params.current || 1, |
| | | pageSize: params.pageSize || params.size || 20, |
| | | ...(params.name !== undefined ? { name: params.name } : {}), |
| | | ...(params.code !== undefined ? { code: params.code } : {}), |
| | | ...(params.memo !== undefined ? { memo: params.memo } : {}), |
| | | ...(params.status !== undefined ? { status: params.status } : {}), |
| | | ...(params.condition !== undefined ? { condition: params.condition } : {}) |
| | | } |
| | | } |
| | | |
| | | function fetchRolePage(params = {}) { |
| | | return request.post({ url: '/role/page', params: buildRolePageParams(params) }) |
| | | } |
| | | |
| | | const fetchRolePrintPage = fetchRolePage |
| | | |
| | | function normalizeRoleManyIds(ids) { |
| | | if (Array.isArray(ids)) { |
| | | return ids |
| | | .map((id) => normalizeLegacyId(id)) |
| | | .filter((id) => id !== '') |
| | | .join(',') |
| | | } |
| | | return normalizeLegacyId(ids) |
| | | } |
| | | |
| | | function fetchGetRoleMany(ids) { |
| | | const normalizedIds = normalizeRoleManyIds(ids) |
| | | return request.post({ url: `/role/many/${normalizedIds}` }) |
| | | } |
| | | |
| | | async function fetchExportRoleReport(payload = {}, options = {}) { |
| | | return fetch(`${import.meta.env.VITE_API_URL}/role/export`, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | ...(options.headers || {}) |
| | | }, |
| | | body: JSON.stringify(payload) |
| | | }) |
| | | } |
| | | |
| | | function fetchGetDeptTree(params) { |
| | | return request.post({ url: '/dept/tree', params }) |
| | | } |
| | | |
| | | function fetchGetMenuTree(params) { |
| | | function fetchGetMenuTree(params = {}) { |
| | | return request.post({ url: '/menu/tree', params }) |
| | | } |
| | | |
| | | function fetchGetRoleScopeList(scopeType, roleId) { |
| | | function fetchSaveMenu(params) { |
| | | return request.post({ url: '/menu/save', params }) |
| | | } |
| | | |
| | | function fetchUpdateMenu(params) { |
| | | return request.post({ url: '/menu/update', params }) |
| | | } |
| | | |
| | | function fetchDeleteMenu(id) { |
| | | return request.post({ url: `/menu/remove/${id}` }) |
| | | } |
| | | |
| | | function assertAdminPasswordUpdatePayload(params) { |
| | | if (!isPlainObject(params)) { |
| | | throw new Error('fetchResetUserPassword requires an object payload') |
| | | } |
| | | if (params.id === undefined || params.id === null || params.id === '') { |
| | | throw new Error('fetchResetUserPassword requires a user id') |
| | | } |
| | | } |
| | | |
| | | function normalizeAdminPasswordUpdateParams(params) { |
| | | assertAdminPasswordUpdatePayload(params) |
| | | if (params.password !== undefined) { |
| | | return params |
| | | } |
| | | if (params.newPassword !== undefined) { |
| | | return { |
| | | ...params, |
| | | password: params.newPassword |
| | | } |
| | | } |
| | | throw new Error('fetchResetUserPassword requires a password payload') |
| | | } |
| | | |
| | | function getScopeListUrl(scopeType) { |
| | | const urlMap = { |
| | | menu: '/role/scope/list', |
| | | pda: '/rolePda/scope/list', |
| | | matnr: '/roleMatnr/scope/list', |
| | | warehouse: '/roleWarehouse/scope/list' |
| | | } |
| | | return request.get({ url: urlMap[scopeType], params: { roleId } }) |
| | | const url = urlMap[scopeType] |
| | | if (!url) { |
| | | throw new Error(`Unsupported scope type: ${scopeType}`) |
| | | } |
| | | return url |
| | | } |
| | | |
| | | function fetchUpdateRoleScope(scopeType, params) { |
| | | function getScopeUpdateUrl(scopeType) { |
| | | const urlMap = { |
| | | menu: '/role/scope/update', |
| | | pda: '/rolePda/scope/update', |
| | | matnr: '/roleMatnr/scope/update', |
| | | warehouse: '/roleWarehouse/scope/update' |
| | | } |
| | | return request.post({ url: urlMap[scopeType], params }) |
| | | const url = urlMap[scopeType] |
| | | if (!url) { |
| | | throw new Error(`Unsupported scope type: ${scopeType}`) |
| | | } |
| | | return url |
| | | } |
| | | |
| | | function getScopeTreeUrl(scopeType) { |
| | | const urlMap = { |
| | | menu: '/menu/tree', |
| | | pda: '/menuPda/tree', |
| | | matnr: '/menuMatnrGroup/tree', |
| | | warehouse: '/menuWarehouse/tree' |
| | | } |
| | | const url = urlMap[scopeType] |
| | | if (!url) { |
| | | throw new Error(`Unsupported scope type: ${scopeType}`) |
| | | } |
| | | return url |
| | | } |
| | | |
| | | function fetchGetRoleScopeList(scopeType, roleId) { |
| | | return request.get({ url: getScopeListUrl(scopeType), params: { roleId } }) |
| | | } |
| | | |
| | | function fetchUpdateRoleScope(scopeType, params) { |
| | | return request.post({ url: getScopeUpdateUrl(scopeType), params }) |
| | | } |
| | | |
| | | function fetchGetRoleScopeTree(scopeType, params = {}) { |
| | | return request.post({ url: getScopeTreeUrl(scopeType), params }) |
| | | } |
| | | |
| | | function fetchGetUserLoginList(params) { |
| | |
| | | }) |
| | | } |
| | | |
| | | function fetchGetMenuList(params) { |
| | | return fetchGetMenuTree(params) |
| | | function fetchGetMenuList(params = {}) { |
| | | return request.post({ url: '/menu/list', params }).then((menuList) => adaptLegacyMenuTree(buildLegacyMenuTree(menuList))) |
| | | } |
| | | |
| | | function adaptLegacyMenuTree(menuTree) { |
| | | if (!Array.isArray(menuTree)) { |
| | | return [] |
| | | } |
| | | |
| | | return menuTree |
| | | .map((node) => adaptLegacyMenuNode(node)) |
| | | .filter(Boolean) |
| | | } |
| | | |
| | | function buildLegacyMenuTree(menuList) { |
| | | if (!Array.isArray(menuList)) { |
| | | return [] |
| | | } |
| | | |
| | | const nodeMap = new Map() |
| | | const roots = [] |
| | | |
| | | menuList.forEach((node) => { |
| | | if (!node || typeof node !== 'object') { |
| | | return |
| | | } |
| | | nodeMap.set(normalizeLegacyId(node.id), { |
| | | ...node, |
| | | children: [] |
| | | }) |
| | | }) |
| | | |
| | | nodeMap.forEach((node) => { |
| | | const parentId = normalizeLegacyId(node.parentId ?? 0) |
| | | if (parentId && parentId !== '0' && nodeMap.has(parentId)) { |
| | | nodeMap.get(parentId).children.push(node) |
| | | return |
| | | } |
| | | roots.push(node) |
| | | }) |
| | | |
| | | return sortLegacyMenuTree(roots) |
| | | } |
| | | |
| | | function sortLegacyMenuTree(nodes) { |
| | | return nodes |
| | | .sort((left, right) => { |
| | | const leftSort = Number(left?.sort ?? 0) |
| | | const rightSort = Number(right?.sort ?? 0) |
| | | if (leftSort !== rightSort) { |
| | | return leftSort - rightSort |
| | | } |
| | | return normalizeLegacyId(left?.id).localeCompare(normalizeLegacyId(right?.id)) |
| | | }) |
| | | .map((node) => ({ |
| | | ...node, |
| | | children: Array.isArray(node.children) ? sortLegacyMenuTree(node.children) : [] |
| | | })) |
| | | } |
| | | |
| | | function adaptLegacyMenuNode(node) { |
| | | if (!node || typeof node !== 'object' || node.type === 1) { |
| | | return null |
| | | } |
| | | |
| | | const menuChildren = [] |
| | | const authList = [] |
| | | const rawChildren = Array.isArray(node.children) ? node.children : [] |
| | | |
| | | rawChildren.forEach((child) => { |
| | | if (!child || typeof child !== 'object') { |
| | | return |
| | | } |
| | | if (child.type === 1) { |
| | | authList.push(buildLegacyAuthItem(child)) |
| | | return |
| | | } |
| | | const adaptedChild = adaptLegacyMenuNode(child) |
| | | if (adaptedChild) { |
| | | menuChildren.push(adaptedChild) |
| | | } |
| | | }) |
| | | |
| | | const adapted = { |
| | | id: normalizeLegacyId(node.id), |
| | | parentId: normalizeLegacyId(node.parentId ?? 0), |
| | | parentName: node.parentName || '', |
| | | name: buildLegacyRouteName(node), |
| | | route: typeof node.route === 'string' ? node.route : '', |
| | | path: buildLegacyMenuPath(node), |
| | | component: buildLegacyMenuComponent(node), |
| | | authority: node.authority || '', |
| | | icon: node.icon || '', |
| | | sort: node.sort ?? 0, |
| | | status: node.status ?? 1, |
| | | memo: node.memo || '', |
| | | type: node.type ?? 0, |
| | | meta: buildLegacyMenuMeta(node), |
| | | children: menuChildren |
| | | } |
| | | if (authList.length > 0) { |
| | | adapted.meta.authList = authList |
| | | } |
| | | |
| | | return adapted |
| | | } |
| | | |
| | | function buildLegacyMenuMeta(node) { |
| | | const metaSource = node.meta && typeof node.meta === 'object' ? node.meta : node |
| | | const meta = { |
| | | title: normalizeLegacyTitle(node.name || metaSource.title || '') |
| | | } |
| | | |
| | | const supportedKeys = [ |
| | | 'icon', |
| | | 'sort', |
| | | 'keepAlive', |
| | | 'isHide', |
| | | 'isHideTab', |
| | | 'fixedTab', |
| | | 'link', |
| | | 'isIframe', |
| | | 'roles', |
| | | 'showBadge', |
| | | 'showTextBadge', |
| | | 'activePath', |
| | | 'isFullPage' |
| | | ] |
| | | |
| | | supportedKeys.forEach((key) => { |
| | | if (metaSource[key] !== undefined) { |
| | | meta[key] = metaSource[key] |
| | | } |
| | | }) |
| | | meta.isEnable = normalizeLegacyEnableState(metaSource) |
| | | |
| | | return meta |
| | | } |
| | | |
| | | function buildLegacyAuthItem(node) { |
| | | const metaSource = node.meta && typeof node.meta === 'object' ? node.meta : node |
| | | return { |
| | | id: normalizeLegacyId(node.id), |
| | | parentId: normalizeLegacyId(node.parentId ?? 0), |
| | | parentName: node.parentName || '', |
| | | name: node.name || metaSource.title || '', |
| | | title: normalizeLegacyTitle(node.name || metaSource.title || ''), |
| | | route: typeof node.route === 'string' ? node.route : '', |
| | | component: buildLegacyMenuComponent(node), |
| | | authMark: metaSource.authMark || metaSource.authority || metaSource.code || '', |
| | | authority: metaSource.authority || metaSource.authMark || metaSource.code || '', |
| | | icon: metaSource.icon || '', |
| | | sort: metaSource.sort ?? 0, |
| | | status: metaSource.status ?? 1, |
| | | memo: metaSource.memo || '', |
| | | type: node.type ?? 1 |
| | | } |
| | | } |
| | | |
| | | function buildLegacyMenuPath(node) { |
| | | const rawPath = typeof node.route === 'string' && node.route.trim() |
| | | ? node.route |
| | | : typeof node.path === 'string' |
| | | ? node.path |
| | | : '' |
| | | return normalizeLegacyPath(rawPath) |
| | | } |
| | | |
| | | function buildLegacyMenuComponent(node) { |
| | | if (typeof node.component === 'string') { |
| | | return node.component |
| | | } |
| | | return '' |
| | | } |
| | | |
| | | function buildLegacyRouteName(node) { |
| | | if (typeof node.name === 'string' && node.name.trim()) { |
| | | return node.name.trim() |
| | | } |
| | | if (typeof node.component === 'string' && node.component.trim()) { |
| | | return node.component.trim() |
| | | } |
| | | const path = buildLegacyMenuPath(node) |
| | | if (path) { |
| | | return path |
| | | .split('/') |
| | | .filter(Boolean) |
| | | .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) |
| | | .join('') |
| | | } |
| | | return `Menu${normalizeLegacyId(node.id)}` |
| | | } |
| | | |
| | | function normalizeLegacyTitle(title) { |
| | | if (typeof title !== 'string') { |
| | | return '' |
| | | } |
| | | const trimmedTitle = title.trim() |
| | | if (trimmedTitle.startsWith('menu.')) { |
| | | return `menus.${trimmedTitle.slice('menu.'.length)}` |
| | | } |
| | | return trimmedTitle |
| | | } |
| | | |
| | | function normalizeLegacyPath(path) { |
| | | if (typeof path !== 'string') { |
| | | return '' |
| | | } |
| | | return path.replace(/^\/+/, '').replace(/\/+$/, '') |
| | | } |
| | | |
| | | function normalizeLegacyId(id) { |
| | | if (id === null || id === undefined || id === '') { |
| | | return '' |
| | | } |
| | | return String(id) |
| | | } |
| | | |
| | | function normalizeLegacyEnableState(metaSource) { |
| | | if (metaSource.isEnable !== undefined) { |
| | | return Boolean(metaSource.isEnable) |
| | | } |
| | | if (metaSource.enabled !== undefined) { |
| | | return Boolean(metaSource.enabled) |
| | | } |
| | | if (metaSource.statusBool !== undefined) { |
| | | return Boolean(metaSource.statusBool) |
| | | } |
| | | if (metaSource.status !== undefined) { |
| | | return Number(metaSource.status) === 1 |
| | | } |
| | | return true |
| | | } |
| | | |
| | | function isPlainObject(value) { |
| | | return Boolean(value) && typeof value === 'object' && !Array.isArray(value) |
| | | } |
| | | |
| | | export { |
| | |
| | | fetchUpdateRole, |
| | | fetchDeleteRole, |
| | | fetchGetRoleOptions, |
| | | fetchRolePrintPage, |
| | | fetchRolePage, |
| | | fetchExportRoleReport, |
| | | fetchGetRoleMany, |
| | | fetchGetDeptTree, |
| | | fetchGetMenuTree, |
| | | fetchSaveMenu, |
| | | fetchUpdateMenu, |
| | | fetchDeleteMenu, |
| | | fetchGetRoleScopeList, |
| | | fetchGetRoleScopeTree, |
| | | fetchUpdateRoleScope, |
| | | fetchGetUserLoginList, |
| | | fetchGetMenuList |
| New file |
| | |
| | | <template> |
| | | <ElSpace wrap> |
| | | <ElButton :disabled="disabled" @click="handleExport">导出</ElButton> |
| | | <ElButton :disabled="disabled" @click="handlePrint">打印</ElButton> |
| | | </ElSpace> |
| | | |
| | | <ListPrintPreviewDialog |
| | | v-model:visible="previewVisibleModel" |
| | | :title="normalizedMeta.reportTitle" |
| | | :meta="normalizedMeta" |
| | | :rows="previewRows" |
| | | :columns="normalizedPreviewColumns" |
| | | :max-preview-rows="previewMaxRows" |
| | | /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from 'vue' |
| | | import ListPrintPreviewDialog from './list-print-preview-dialog.vue' |
| | | import { |
| | | buildListExportPayload, |
| | | buildListPrintPayload, |
| | | buildPreviewColumns, |
| | | buildReportStyleMeta |
| | | } from './list-export-print.helpers.js' |
| | | |
| | | defineOptions({ name: 'ListExportPrint' }) |
| | | |
| | | const props = defineProps({ |
| | | reportTitle: { type: String, default: '报表' }, |
| | | selectedRows: { type: Array, default: () => [] }, |
| | | queryParams: { type: Object, default: () => ({}) }, |
| | | columns: { type: Array, default: () => [] }, |
| | | previewRows: { type: Array, default: () => [] }, |
| | | previewMeta: { type: Object, default: () => ({}) }, |
| | | previewMaxRows: { type: Number, default: 50 }, |
| | | total: { type: Number, default: 0 }, |
| | | maxResults: { type: Number, default: 1000 }, |
| | | disabled: { type: Boolean, default: false } |
| | | }) |
| | | |
| | | const emit = defineEmits(['export', 'print']) |
| | | const normalizedMeta = computed(() => |
| | | buildReportStyleMeta({ |
| | | reportTitle: props.reportTitle, |
| | | meta: props.previewMeta |
| | | }) |
| | | ) |
| | | const normalizedPreviewColumns = computed(() => |
| | | buildPreviewColumns({ |
| | | columns: props.columns, |
| | | reportStyle: normalizedMeta.value.reportStyle |
| | | }) |
| | | ) |
| | | const previewVisibleModel = defineModel('previewVisible', { |
| | | type: Boolean, |
| | | default: false |
| | | }) |
| | | |
| | | const handleExport = () => { |
| | | const payload = buildListExportPayload({ |
| | | reportTitle: props.reportTitle, |
| | | selectedRows: props.selectedRows, |
| | | queryParams: props.queryParams, |
| | | columns: props.columns, |
| | | meta: normalizedMeta.value |
| | | }) |
| | | emit('export', payload) |
| | | } |
| | | |
| | | const handlePrint = () => { |
| | | const payload = buildListPrintPayload({ |
| | | selectedRows: props.selectedRows, |
| | | queryParams: props.queryParams, |
| | | total: props.total, |
| | | maxResults: props.maxResults |
| | | }) |
| | | emit('print', payload) |
| | | } |
| | | </script> |
| New file |
| | |
| | | const DEFAULT_MAX_RESULTS = 1000 |
| | | const DEFAULT_PRINT_PAGE_SIZE = 20 |
| | | const HIDDEN_EXPORT_COLUMNS = new Set(['selection', 'operation']) |
| | | const DEFAULT_REPORT_STYLE = { |
| | | titleAlign: 'center', |
| | | titleLevel: 'strong', |
| | | orientation: 'portrait', |
| | | density: 'compact', |
| | | showSequence: true, |
| | | showBorder: true |
| | | } |
| | | |
| | | function normalizeSelectedIds(selectedRows = []) { |
| | | if (!Array.isArray(selectedRows)) { |
| | | return [] |
| | | } |
| | | |
| | | return selectedRows |
| | | .map((row) => row?.id) |
| | | .filter((id) => id !== void 0 && id !== null) |
| | | } |
| | | |
| | | function normalizeFlatQueryParams(queryParams = {}) { |
| | | if (!queryParams || typeof queryParams !== 'object') { |
| | | return {} |
| | | } |
| | | |
| | | const { current, pageSize, size, ...rest } = queryParams |
| | | const normalizedPageSize = pageSize ?? size |
| | | return { |
| | | ...(current !== void 0 ? { current } : {}), |
| | | ...(normalizedPageSize !== void 0 ? { pageSize: normalizedPageSize } : {}), |
| | | ...rest |
| | | } |
| | | } |
| | | |
| | | export function toExportColumns(columns = []) { |
| | | if (!Array.isArray(columns)) { |
| | | return [] |
| | | } |
| | | |
| | | return columns |
| | | .map((column) => { |
| | | if (!column || typeof column !== 'object') { |
| | | return null |
| | | } |
| | | |
| | | const source = column.source ?? column.prop |
| | | const label = column.label |
| | | if (!source || !label || HIDDEN_EXPORT_COLUMNS.has(source)) { |
| | | return null |
| | | } |
| | | |
| | | return { |
| | | source, |
| | | label, |
| | | ...(column.align !== void 0 ? { align: column.align } : {}) |
| | | } |
| | | }) |
| | | .filter(Boolean) |
| | | } |
| | | |
| | | export function buildReportStyleMeta({ reportTitle = '', meta = {} } = {}) { |
| | | const reportStyle = { |
| | | ...DEFAULT_REPORT_STYLE, |
| | | ...(meta?.reportStyle ?? {}) |
| | | } |
| | | |
| | | return { |
| | | ...meta, |
| | | reportTitle, |
| | | reportStyle |
| | | } |
| | | } |
| | | |
| | | export function buildPreviewColumns({ columns = [], reportStyle = {} } = {}) { |
| | | const normalizedColumns = toExportColumns(columns) |
| | | |
| | | if (reportStyle?.showSequence === false) { |
| | | return normalizedColumns |
| | | } |
| | | |
| | | return [ |
| | | { source: '__sequence__', label: '序号', align: 'center' }, |
| | | ...normalizedColumns |
| | | ] |
| | | } |
| | | |
| | | export function buildListExportPayload({ |
| | | reportTitle = '', |
| | | selectedRows = [], |
| | | queryParams = {}, |
| | | columns = [], |
| | | meta = {} |
| | | } = {}) { |
| | | const ids = normalizeSelectedIds(selectedRows) |
| | | const normalizedColumns = toExportColumns(columns) |
| | | const reportMeta = { |
| | | reportTitle, |
| | | ...meta |
| | | } |
| | | |
| | | if (ids.length > 0) { |
| | | return { |
| | | ids, |
| | | columns: normalizedColumns, |
| | | meta: reportMeta |
| | | } |
| | | } |
| | | |
| | | return { |
| | | ...normalizeFlatQueryParams(queryParams), |
| | | ids, |
| | | columns: normalizedColumns, |
| | | meta: reportMeta |
| | | } |
| | | } |
| | | |
| | | export function buildPrintPageQuery({ queryParams = {}, total = 0, maxResults = DEFAULT_MAX_RESULTS } = {}) { |
| | | const normalizedQueryParams = normalizeFlatQueryParams(queryParams) |
| | | const normalizedTotal = Number(total) |
| | | const parsedMaxResults = Number(maxResults) |
| | | const normalizedMaxResults = |
| | | Number.isInteger(parsedMaxResults) && parsedMaxResults > 0 ? parsedMaxResults : DEFAULT_MAX_RESULTS |
| | | const fallbackPageSize = Number(normalizedQueryParams.pageSize) |
| | | |
| | | let pageSize = DEFAULT_PRINT_PAGE_SIZE |
| | | if (Number.isFinite(normalizedTotal) && normalizedTotal > 0) { |
| | | pageSize = Math.min(normalizedTotal, normalizedMaxResults) |
| | | } else if (Number.isFinite(fallbackPageSize) && fallbackPageSize > 0) { |
| | | pageSize = Math.min(fallbackPageSize, normalizedMaxResults) |
| | | } |
| | | |
| | | return { |
| | | ...normalizedQueryParams, |
| | | current: 1, |
| | | pageSize |
| | | } |
| | | } |
| | | |
| | | export function buildListPrintPayload({ |
| | | selectedRows = [], |
| | | queryParams = {}, |
| | | total = 0, |
| | | maxResults = DEFAULT_MAX_RESULTS |
| | | } = {}) { |
| | | const ids = normalizeSelectedIds(selectedRows) |
| | | if (ids.length > 0) { |
| | | return { ids } |
| | | } |
| | | |
| | | return buildPrintPageQuery({ queryParams, total, maxResults }) |
| | | } |
| | | |
| | | export { DEFAULT_MAX_RESULTS } |
| New file |
| | |
| | | function escapeHtml(value) { |
| | | return String(value ?? '') |
| | | .replaceAll('&', '&') |
| | | .replaceAll('<', '<') |
| | | .replaceAll('>', '>') |
| | | .replaceAll('"', '"') |
| | | .replaceAll("'", ''') |
| | | } |
| | | |
| | | function getReportStyle(meta = {}) { |
| | | return meta?.reportStyle && typeof meta.reportStyle === 'object' ? meta.reportStyle : {} |
| | | } |
| | | |
| | | function getTitleAlignClass(titleAlign) { |
| | | const alignMap = { |
| | | left: 'title-left', |
| | | center: 'title-center', |
| | | right: 'title-right' |
| | | } |
| | | return alignMap[titleAlign] ?? alignMap.center |
| | | } |
| | | |
| | | function getTitleLevelClass(titleLevel) { |
| | | const levelMap = { |
| | | normal: 'title-normal', |
| | | strong: 'title-strong', |
| | | prominent: 'title-prominent' |
| | | } |
| | | return levelMap[titleLevel] ?? levelMap.strong |
| | | } |
| | | |
| | | function getPageOrientation(reportStyle = {}) { |
| | | return reportStyle.orientation === 'landscape' ? 'landscape' : 'portrait' |
| | | } |
| | | |
| | | function getMetaItems(meta = {}) { |
| | | return [ |
| | | { key: 'reportDate', label: '报表日期', value: meta.reportDate ?? '--' }, |
| | | { key: 'operator', label: '打印人', value: meta.operator ?? '--' }, |
| | | { key: 'printedAt', label: '打印时间', value: meta.printedAt ?? '--' }, |
| | | { key: 'count', label: '记录数', value: meta.count ?? '--' } |
| | | ] |
| | | } |
| | | |
| | | function getCellText(row, column, index) { |
| | | if (column?.source === '__sequence__') { |
| | | return index + 1 |
| | | } |
| | | return row?.[column?.source] ?? '--' |
| | | } |
| | | |
| | | function getAlignClass(column = {}) { |
| | | const alignMap = { |
| | | left: 'cell-left', |
| | | center: 'cell-center', |
| | | right: 'cell-right' |
| | | } |
| | | return alignMap[column.align] ?? alignMap.left |
| | | } |
| | | |
| | | export function buildPrintDocumentHtml({ title = '报表', meta = {}, rows = [], columns = [] } = {}) { |
| | | const reportStyle = getReportStyle(meta) |
| | | const reportTitle = meta.reportTitle || title |
| | | const titleClass = `${getTitleAlignClass(reportStyle.titleAlign)} ${getTitleLevelClass(reportStyle.titleLevel)}` |
| | | const orientation = getPageOrientation(reportStyle) |
| | | const showBorder = reportStyle.showBorder !== false |
| | | const metaItems = getMetaItems(meta) |
| | | const tableWrapClass = showBorder ? 'report-table-wrap' : 'report-table-wrap report-table-wrap-borderless' |
| | | const cellClassName = showBorder ? 'cell-bordered' : 'cell-borderless' |
| | | |
| | | const headerHtml = columns |
| | | .map( |
| | | (column) => |
| | | `<th class="${getAlignClass(column)} ${cellClassName}">${escapeHtml(column.label ?? '--')}</th>` |
| | | ) |
| | | .join('') |
| | | |
| | | const bodyHtml = |
| | | rows.length > 0 |
| | | ? rows |
| | | .map((row, index) => { |
| | | const cells = columns |
| | | .map( |
| | | (column) => |
| | | `<td class="${getAlignClass(column)} ${cellClassName}">${escapeHtml(getCellText(row, column, index))}</td>` |
| | | ) |
| | | .join('') |
| | | return `<tr>${cells}</tr>` |
| | | }) |
| | | .join('') |
| | | : `<tr><td colspan="${Math.max(columns.length, 1)}" class="empty-cell">暂无打印数据</td></tr>` |
| | | |
| | | const metaHtml = metaItems |
| | | .map( |
| | | (item) => |
| | | `<div class="report-meta-item"><span class="report-meta-label">${escapeHtml(item.label)}</span><span class="report-meta-value">${escapeHtml(item.value)}</span></div>` |
| | | ) |
| | | .join('') |
| | | |
| | | return `<!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="UTF-8" /> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| | | <title>${escapeHtml(reportTitle)}</title> |
| | | <style> |
| | | :root { |
| | | color-scheme: light; |
| | | } |
| | | * { |
| | | box-sizing: border-box; |
| | | } |
| | | @page { |
| | | size: A4 ${orientation}; |
| | | } |
| | | html, |
| | | body { |
| | | margin: 0; |
| | | padding: 0; |
| | | background: #ffffff; |
| | | color: #0f172a; |
| | | font-family: "Microsoft YaHei", "PingFang SC", "Helvetica Neue", Arial, sans-serif; |
| | | -webkit-print-color-adjust: exact; |
| | | print-color-adjust: exact; |
| | | } |
| | | body { |
| | | padding: 0; |
| | | } |
| | | .report-page { |
| | | width: 100%; |
| | | } |
| | | .report-title-wrap { |
| | | border-bottom: 1px solid #cbd5e1; |
| | | padding-bottom: 16px; |
| | | } |
| | | .report-title { |
| | | line-height: 1.2; |
| | | color: #0f172a; |
| | | } |
| | | .title-left { |
| | | text-align: left; |
| | | } |
| | | .title-center { |
| | | text-align: center; |
| | | } |
| | | .title-right { |
| | | text-align: right; |
| | | } |
| | | .title-normal { |
| | | font-size: 18px; |
| | | font-weight: 500; |
| | | } |
| | | .title-strong { |
| | | font-size: 22px; |
| | | font-weight: 600; |
| | | } |
| | | .title-prominent { |
| | | font-size: 26px; |
| | | font-weight: 700; |
| | | } |
| | | .report-meta { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | margin-top: 12px; |
| | | font-size: 11px; |
| | | color: #475569; |
| | | } |
| | | .report-meta-item { |
| | | min-width: 0; |
| | | } |
| | | .report-meta-label { |
| | | color: #94a3b8; |
| | | } |
| | | .report-meta-value { |
| | | margin-left: 8px; |
| | | color: #334155; |
| | | word-break: break-word; |
| | | } |
| | | .report-table-wrap { |
| | | margin-top: 16px; |
| | | border: 1px solid #cbd5e1; |
| | | overflow: hidden; |
| | | } |
| | | .report-table-wrap-borderless { |
| | | border: 0; |
| | | } |
| | | table { |
| | | width: 100%; |
| | | border-collapse: collapse; |
| | | table-layout: auto; |
| | | font-size: 11px; |
| | | } |
| | | th, |
| | | td { |
| | | padding: 4px 6px; |
| | | vertical-align: top; |
| | | word-break: break-all; |
| | | } |
| | | .cell-bordered { |
| | | border: 1px solid #cbd5e1; |
| | | } |
| | | .cell-borderless { |
| | | border: 0; |
| | | } |
| | | th { |
| | | background: #f1f5f9; |
| | | color: #475569; |
| | | font-weight: 600; |
| | | } |
| | | td { |
| | | color: #334155; |
| | | } |
| | | .cell-left { |
| | | text-align: left; |
| | | } |
| | | .cell-center { |
| | | text-align: center; |
| | | } |
| | | .cell-right { |
| | | text-align: right; |
| | | } |
| | | .empty-cell { |
| | | padding: 32px 12px; |
| | | text-align: center; |
| | | color: #94a3b8; |
| | | } |
| | | @media print { |
| | | html, |
| | | body { |
| | | background: #ffffff; |
| | | } |
| | | body { |
| | | padding: 0; |
| | | } |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div class="report-page"> |
| | | <div class="report-title-wrap"> |
| | | <div class="report-title ${titleClass}">${escapeHtml(reportTitle)}</div> |
| | | </div> |
| | | <div class="report-meta">${metaHtml}</div> |
| | | <div class="${tableWrapClass}"> |
| | | <table> |
| | | <thead> |
| | | <tr>${headerHtml}</tr> |
| | | </thead> |
| | | <tbody>${bodyHtml}</tbody> |
| | | </table> |
| | | </div> |
| | | </div> |
| | | <script> |
| | | window.addEventListener('load', () => { |
| | | window.focus(); |
| | | setTimeout(() => { |
| | | window.print(); |
| | | }, 50); |
| | | }); |
| | | window.addEventListener('afterprint', () => { |
| | | window.close(); |
| | | }); |
| | | </script> |
| | | </body> |
| | | </html>` |
| | | } |
| | | |
| | | export function printReportDocument(payload = {}) { |
| | | if (typeof window === 'undefined') { |
| | | return |
| | | } |
| | | |
| | | const printWindow = window.open('', '_blank') |
| | | if (!printWindow) { |
| | | return |
| | | } |
| | | |
| | | printWindow.document.open() |
| | | printWindow.document.write(buildPrintDocumentHtml(payload)) |
| | | printWindow.document.close() |
| | | } |
| New file |
| | |
| | | <template> |
| | | <ElDialog |
| | | v-model="visible" |
| | | :title="title" |
| | | width="min(96vw, 1100px)" |
| | | top="4vh" |
| | | class="max-h-[88vh] overflow-hidden" |
| | | > |
| | | <div class="flex max-h-[calc(88vh-160px)] min-h-0 flex-col bg-slate-100 px-4 py-4 md:px-6 md:py-6"> |
| | | <div |
| | | :class="paperClass" |
| | | :style="paperStyle" |
| | | class="mx-auto flex min-h-0 flex-1 flex-col overflow-hidden bg-white px-7 py-6 text-slate-800 shadow-sm md:px-8 md:py-7" |
| | | > |
| | | <div class="flex flex-wrap items-center justify-end gap-2 pb-2"> |
| | | <ElRadioGroup v-model="currentShowBorder" size="small"> |
| | | <ElRadioButton :label="true">边框开</ElRadioButton> |
| | | <ElRadioButton :label="false">边框关</ElRadioButton> |
| | | </ElRadioGroup> |
| | | |
| | | <ElRadioGroup v-model="currentOrientation" size="small"> |
| | | <ElRadioButton label="portrait">竖版</ElRadioButton> |
| | | <ElRadioButton label="landscape">横版</ElRadioButton> |
| | | </ElRadioGroup> |
| | | </div> |
| | | |
| | | <div class="border-b border-slate-300 pb-3"> |
| | | <div :class="titleClass">{{ meta.reportTitle ?? '--' }}</div> |
| | | </div> |
| | | |
| | | <div class="mt-3 grid grid-cols-4 gap-2 text-[11px] leading-tight text-slate-600"> |
| | | <div v-for="item in metaItems" :key="item.key" class="min-w-0"> |
| | | <span class="text-slate-400">{{ item.label }}</span> |
| | | <span class="ml-2 break-words text-slate-700">{{ item.value ?? '--' }}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div |
| | | v-if="hiddenRowCount > 0" |
| | | class="mt-3 rounded-md border border-amber-200 bg-amber-50 px-2.5 py-1.5 text-[11px] leading-tight text-amber-700" |
| | | > |
| | | 预览仅展示前 {{ previewRows.length }} 条,点击打印将输出全部 {{ rows.length }} 条数据。 |
| | | </div> |
| | | |
| | | <div :class="tableWrapClass" class="mt-4 min-h-0 flex-1 overflow-auto"> |
| | | <table class="w-full table-auto border-collapse text-left text-[11px] leading-tight"> |
| | | <thead class="bg-slate-100 text-slate-600"> |
| | | <tr> |
| | | <th |
| | | v-for="column in columns" |
| | | :key="column.source" |
| | | :class="headerCellClass" |
| | | > |
| | | {{ column.label }} |
| | | </th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr v-if="previewRows.length === 0"> |
| | | <td |
| | | :colspan="Math.max(columns.length, 1)" |
| | | :class="emptyCellClass" |
| | | > |
| | | 暂无打印数据 |
| | | </td> |
| | | </tr> |
| | | <tr v-for="(row, index) in previewRows" :key="row?.id ?? index"> |
| | | <td |
| | | v-for="column in columns" |
| | | :key="column.source" |
| | | :class="bodyCellClass" |
| | | > |
| | | {{ column.source === '__sequence__' ? index + 1 : row?.[column.source] ?? '--' }} |
| | | </td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="flex items-center justify-end gap-2 print:hidden"> |
| | | <ElButton @click="visible = false">关闭</ElButton> |
| | | <ElButton type="primary" @click="handlePrint">打印</ElButton> |
| | | </div> |
| | | </template> |
| | | </ElDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref, watch } from 'vue' |
| | | import { printReportDocument } from './list-print-document.js' |
| | | |
| | | defineOptions({ name: 'ListPrintPreviewDialog' }) |
| | | |
| | | const visible = defineModel('visible', { type: Boolean, default: false }) |
| | | |
| | | const props = defineProps({ |
| | | title: { type: String, default: '打印预览' }, |
| | | meta: { type: Object, default: () => ({}) }, |
| | | rows: { type: Array, default: () => [] }, |
| | | columns: { type: Array, default: () => [] }, |
| | | maxPreviewRows: { type: Number, default: 50 } |
| | | }) |
| | | |
| | | const meta = computed(() => (props.meta && typeof props.meta === 'object' ? props.meta : {})) |
| | | const reportStyleSource = computed(() => |
| | | meta.value.reportStyle && typeof meta.value.reportStyle === 'object' |
| | | ? meta.value.reportStyle |
| | | : {} |
| | | ) |
| | | const currentOrientation = ref('portrait') |
| | | const currentShowBorder = ref(true) |
| | | |
| | | const metaItems = computed(() => { |
| | | return [ |
| | | { key: 'reportDate', label: '报表日期', value: meta.value.reportDate }, |
| | | { key: 'operator', label: '打印人', value: meta.value.operator }, |
| | | { key: 'printedAt', label: '打印时间', value: meta.value.printedAt }, |
| | | { key: 'count', label: '记录数', value: meta.value.count } |
| | | ] |
| | | }) |
| | | const previewRows = computed(() => props.rows.slice(0, props.maxPreviewRows)) |
| | | const hiddenRowCount = computed(() => Math.max(props.rows.length - previewRows.value.length, 0)) |
| | | const effectiveMeta = computed(() => ({ |
| | | ...meta.value, |
| | | reportStyle: { |
| | | ...reportStyleSource.value, |
| | | orientation: currentOrientation.value, |
| | | showBorder: currentShowBorder.value |
| | | } |
| | | })) |
| | | const paperClass = computed(() => |
| | | currentOrientation.value === 'landscape' |
| | | ? 'w-full max-w-[980px]' |
| | | : 'w-full max-w-[760px]' |
| | | ) |
| | | const paperStyle = computed(() => ({ |
| | | aspectRatio: currentOrientation.value === 'landscape' ? '297 / 210' : '210 / 297', |
| | | width: |
| | | currentOrientation.value === 'landscape' |
| | | ? 'min(100%, calc((88vh - 240px) * 297 / 210))' |
| | | : 'min(100%, calc((88vh - 240px) * 210 / 297))' |
| | | })) |
| | | |
| | | const titleClass = computed(() => { |
| | | const alignMap = { |
| | | left: 'text-left', |
| | | center: 'text-center', |
| | | right: 'text-right' |
| | | } |
| | | const levelMap = { |
| | | normal: 'text-[18px] font-medium', |
| | | strong: 'text-[22px] font-semibold', |
| | | prominent: 'text-[26px] font-bold' |
| | | } |
| | | const reportStyle = reportStyleSource.value |
| | | const alignClass = alignMap[reportStyle.titleAlign] ?? alignMap.center |
| | | const levelClass = levelMap[reportStyle.titleLevel] ?? levelMap.strong |
| | | return `${alignClass} ${levelClass} leading-tight text-slate-900` |
| | | }) |
| | | const tableWrapClass = computed(() => |
| | | currentShowBorder.value ? 'border border-slate-300' : 'border border-transparent' |
| | | ) |
| | | const headerCellClass = computed(() => |
| | | currentShowBorder.value |
| | | ? 'border border-slate-300 px-1.5 py-1.5 font-medium break-all' |
| | | : 'border-0 px-1.5 py-1.5 font-medium break-all' |
| | | ) |
| | | const bodyCellClass = computed(() => |
| | | currentShowBorder.value |
| | | ? 'border border-slate-300 px-1.5 py-1.5 text-slate-700 break-all align-top' |
| | | : 'border-0 px-1.5 py-1.5 text-slate-700 break-all align-top' |
| | | ) |
| | | const emptyCellClass = computed(() => |
| | | currentShowBorder.value |
| | | ? 'border border-slate-300 px-3 py-6 text-center text-slate-400' |
| | | : 'border-0 px-3 py-6 text-center text-slate-400' |
| | | ) |
| | | |
| | | watch( |
| | | reportStyleSource, |
| | | (reportStyle) => { |
| | | currentOrientation.value = reportStyle.orientation === 'landscape' ? 'landscape' : 'portrait' |
| | | currentShowBorder.value = reportStyle.showBorder !== false |
| | | }, |
| | | { immediate: true, deep: true } |
| | | ) |
| | | |
| | | const handlePrint = () => { |
| | | printReportDocument({ |
| | | title: props.title, |
| | | meta: effectiveMeta.value, |
| | | rows: props.rows, |
| | | columns: props.columns |
| | | }) |
| | | } |
| | | </script> |
| | |
| | | @contextmenu.prevent="(e) => showMenu(e, item.path)" |
| | | > |
| | | <ArtSvgIcon |
| | | v-show="item.icon" |
| | | v-if="item.icon" |
| | | :icon="item.icon" |
| | | class="text-base mr-1 group-hover:text-theme" |
| | | :class="item.path === activeTab ? 'text-theme' : 'text-g-600'" |
| | |
| | | prefix: 'icon-park-outline', |
| | | width: 48 |
| | | }, |
| | | 'line-md': { |
| | | height: 24, |
| | | icons: { |
| | | 'coffee-half-empty-filled-loop': { |
| | | body: '<defs><mask id="SVG5AkzhcyZ"><path fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M12 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M16 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4"><animate attributeName="d" dur="3s" repeatCount="indefinite" values="M8 0c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M12 0c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M16 0c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4;M8 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M12 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4M16 -8c0 2 -2 2 -2 4s2 2 2 4s-2 2 -2 4s2 2 2 4"/></path><path d="M4 7h16v0h-16v12h16v-32h-16Z"><animate fill="freeze" attributeName="d" begin="1s" dur="0.6s" to="M4 2h16v5h-16v12h16v-24h-16Z"/></path></mask></defs><path fill="currentColor" fill-opacity="0" d="M17 14v4c0 1.66 -1.34 3 -3 3h-6c-1.66 0 -3 -1.34 -3 -3v-4Z"><animate fill="freeze" attributeName="fill-opacity" begin="1.6s" dur="0.4s" to="1"/></path><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="48" d="M17 9v9c0 1.66 -1.34 3 -3 3h-6c-1.66 0 -3 -1.34 -3 -3v-9Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="48;0"/></path><path stroke-dasharray="16" stroke-dashoffset="16" d="M17 9h3c0.55 0 1 0.45 1 1v3c0 0.55 -0.45 1 -1 1h-3"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.6s" dur="0.3s" to="0"/></path></g><path fill="currentColor" d="M0 0h24v24H0z" mask="url(#SVG5AkzhcyZ)"/>' |
| | | }, |
| | | 'github-twotone': { |
| | | body: '<path fill="currentColor" fill-opacity="0" d="M15 4.5c-0.39 -0.1 -1.33 -0.5 -3 -0.5c-1.67 0 -2.61 0.4 -3 0.5c-0.53 -0.43 -1.94 -1.5 -3.5 -1.5c-0.34 1 -0.29 2.22 0 3c-0.75 1 -1 2 -1 3.5c0 2.19 0.48 3.58 1.5 4.5c1.02 0.92 2.11 1.37 3.5 1.5c-0.65 0.54 -0.5 1.87 -0.5 2.5v4h6v-4c0 -0.63 0.15 -1.96 -0.5 -2.5c1.39 -0.13 2.48 -0.58 3.5 -1.5c1.02 -0.92 1.5 -2.31 1.5 -4.5c0 -1.5 -0.25 -2.5 -1 -3.5c0.29 -0.78 0.34 -2 0 -3c-1.56 0 -2.97 1.07 -3.5 1.5Z"><animate fill="freeze" attributeName="fill-opacity" begin="0.9s" dur="0.15s" to=".3"/></path><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="32" d="M12 4c1.67 0 2.61 0.4 3 0.5c0.53 -0.43 1.94 -1.5 3.5 -1.5c0.34 1 0.29 2.22 0 3c0.75 1 1 2 1 3.5c0 2.19 -0.48 3.58 -1.5 4.5c-1.02 0.92 -2.11 1.37 -3.5 1.5c0.65 0.54 0.5 1.87 0.5 2.5c0 0.73 0 3 0 3M12 4c-1.67 0 -2.61 0.4 -3 0.5c-0.53 -0.43 -1.94 -1.5 -3.5 -1.5c-0.34 1 -0.29 2.22 0 3c-0.75 1 -1 2 -1 3.5c0 2.19 0.48 3.58 1.5 4.5c1.02 0.92 2.11 1.37 3.5 1.5c-0.65 0.54 -0.5 1.87 -0.5 2.5c0 0.73 0 3 0 3"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="32;0"/></path><path stroke-dasharray="10" stroke-dashoffset="10" d="M9 19c-1.41 0 -2.84 -0.56 -3.69 -1.19c-0.84 -0.63 -1.09 -1.66 -2.31 -2.31"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.7s" dur="0.2s" to="0"/></path></g>' |
| | | }, |
| | | 'phone-call-twotone-loop': { |
| | | body: '<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path fill="currentColor" fill-opacity="0" stroke-dasharray="62" d="M8 3c0.5 0 2.5 4.5 2.5 5c0 1 -1.5 2 -2 3c-0.5 1 0.5 2 1.5 3c0.39 0.39 2 2 3 1.5c1 -0.5 2 -2 3 -2c0.5 0 5 2 5 2.5c0 2 -1.5 3.5 -3 4c-1.5 0.5 -2.5 0.5 -4.5 0c-2 -0.5 -3.5 -1 -6 -3.5c-2.5 -2.5 -3 -4 -3.5 -6c-0.5 -2 -0.5 -3 0 -4.5c0.5 -1.5 2 -3 4 -3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="62;0"/><animateTransform attributeName="transform" dur="2.7s" keyTimes="0;0.035;0.07;0.105;0.14;0.175;0.21;0.245;0.28;1" repeatCount="indefinite" type="rotate" values="0 12 12;15 12 12;0 12 12;-12 12 12;0 12 12;12 12 12;0 12 12;-15 12 12;0 12 12;0 12 12"/><animate fill="freeze" attributeName="fill-opacity" begin="1.3s" dur="0.15s" to=".3"/></path><g fill="none"><path stroke-dasharray="6" stroke-dashoffset="6" d="M15.76 8.28c-0.5 -0.51 -1.1 -0.93 -1.76 -1.24M15.76 8.28c0.49 0.49 0.9 1.08 1.2 1.72"><animate attributeName="stroke-dashoffset" begin="0.7s" dur="2.7s" keyTimes="0;0.15;0.3;1" repeatCount="indefinite" values="6;0;6;6"/></path><path stroke-dasharray="8" stroke-dashoffset="8" d="M18.67 5.35c-1 -1 -2.26 -1.73 -3.67 -2.1M18.67 5.35c0.99 1 1.72 2.25 2.08 3.65"><animate attributeName="stroke-dashoffset" begin="1s" dur="2.7s" keyTimes="0;0.15;0.3;1" repeatCount="indefinite" values="8;0;8;8"/></path></g></g>' |
| | | }, |
| | | 'reddit-loop': { |
| | | body: '<defs><mask id="SVGC2McScuF"><g fill="#fff"><path fill-opacity="0" stroke="#fff" stroke-dasharray="46" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9.42c4.42 0 8 2.37 8 5.29c0 2.92 -3.58 5.29 -8 5.29c-4.42 0 -8 -2.37 -8 -5.29c0 -2.92 3.58 -5.29 8 -5.29Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="46;0"/><animate fill="freeze" attributeName="fill-opacity" begin="0.6s" dur="0.4s" to="1"/></path><path d="M3.94 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24ZM20.06 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24Z" opacity="0"><set fill="freeze" attributeName="opacity" begin="1s" to="1"/><animate fill="freeze" attributeName="d" begin="1s" dur="0.2s" values="M7.24 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24ZM16.76 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24Z;M3.94 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24ZM20.06 9.73c1.24 0 2.24 1 2.24 2.24c0 1.24 -1 2.24 -2.24 2.24c-1.24 0 -2.24 -1 -2.24 -2.24c0 -1.24 1 -2.24 2.24 -2.24Z"/></path><circle cx="18.45" cy="4.23" r="1.61" opacity="0"><animate attributeName="cx" begin="2s" dur="6s" keyTimes="0;0.5;1" repeatCount="indefinite" values="18.45;5.75;18.45"/><set fill="freeze" attributeName="opacity" begin="2.2s" to="1"/></circle></g><circle cx="8.45" cy="13.59" r="1.61" opacity="0"><animate fill="freeze" attributeName="opacity" begin="1.2s" dur="0.4s" to="1"/></circle><circle cx="15.55" cy="13.59" r="1.61" opacity="0"><animate fill="freeze" attributeName="opacity" begin="1.6s" dur="0.4s" to="1"/></circle><g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width=".8"><path stroke="#fff" stroke-dasharray="14" stroke-dashoffset="14" d="M12 8.75l1.18 -5.64l5.03 1.07"><animate attributeName="d" begin="2s" dur="6s" keyTimes="0;0.25;0.5;0.75;1" repeatCount="indefinite" values="M12 8.75l1.18 -5.64l5.03 1.07;M12 8.75l0 -6.75l0 2.18;M12 8.75l-1.18 -5.64l-5.03 1.07;M12 8.75l0 -6.75l0 2.18;M12 8.75l1.18 -5.64l5.03 1.07"/><animate fill="freeze" attributeName="stroke-dashoffset" begin="2s" dur="0.2s" to="0"/></path><path stroke="#000" stroke-dasharray="10" stroke-dashoffset="10" d="M8.47 17.52c0 0 0.94 1.06 3.53 1.06c2.58 0 3.53 -1.06 3.53 -1.06"><animate fill="freeze" attributeName="stroke-dashoffset" begin="2s" dur="0.2s" to="0"/></path></g></mask></defs><path fill="currentColor" d="M0 0h24v24H0z" mask="url(#SVGC2McScuF)"/>' |
| | | }, |
| | | 'sun-rising-filled-loop': { |
| | | body: '<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path fill="currentColor" d="M12 6c3.31 0 6 2.69 6 6c0 3.31 -2.69 6 -6 6c-3.31 0 -6 -2.69 -6 -6c0 -3.31 2.69 -6 6 -6Z"><animate fill="freeze" attributeName="d" dur="0.6s" values="M12 26c3.31 0 6 2.69 6 6c0 3.31 -2.69 6 -6 6c-3.31 0 -6 -2.69 -6 -6c0 -3.31 2.69 -6 6 -6Z;M12 6c3.31 0 6 2.69 6 6c0 3.31 -2.69 6 -6 6c-3.31 0 -6 -2.69 -6 -6c0 -3.31 2.69 -6 6 -6Z"/></path><g fill="none"><path d="M12 21v1M21 12h1M12 3v-1M3 12h-1" opacity="0"><animateTransform attributeName="transform" dur="30s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/><set fill="freeze" attributeName="opacity" begin="0.7s" to="1"/><animate fill="freeze" attributeName="d" begin="0.7s" dur="0.2s" values="M12 19v1M19 12h1M12 5v-1M5 12h-1;M12 21v1M21 12h1M12 3v-1M3 12h-1"/></path><path d="M18.5 18.5l0.5 0.5M18.5 5.5l0.5 -0.5M5.5 5.5l-0.5 -0.5M5.5 18.5l-0.5 0.5" opacity="0"><animateTransform attributeName="transform" dur="30s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/><set fill="freeze" attributeName="opacity" begin="0.9s" to="1"/><animate fill="freeze" attributeName="d" begin="0.9s" dur="0.2s" values="M17 17l0.5 0.5M17 7l0.5 -0.5M7 7l-0.5 -0.5M7 17l-0.5 0.5;M18.5 18.5l0.5 0.5M18.5 5.5l0.5 -0.5M5.5 5.5l-0.5 -0.5M5.5 18.5l-0.5 0.5"/></path></g></g>' |
| | | }, |
| | | 'switch-off': { |
| | | body: '<path fill="none" stroke="currentColor" stroke-dasharray="54" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 7h5c2.76 0 5 2.24 5 5c0 2.76 -2.24 5 -5 5h-10c-2.76 0 -5 -2.24 -5 -5c0 -2.76 2.24 -5 5 -5Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="54;0"/></path><circle cx="7" cy="12" r="3" fill="currentColor" opacity="0"><animate fill="freeze" attributeName="opacity" begin="0.6s" dur="0.2s" to="1"/></circle>' |
| | | }, |
| | | 'volume-high-filled': { |
| | | body: '<g fill="currentColor"><path fill-opacity="0" stroke="currentColor" stroke-dasharray="34" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 10h3.5l3.5 -3.5v10.5l-3.5 -3.5h-3.5Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.4s" values="34;0"/><animate fill="freeze" attributeName="fill-opacity" begin="0.8s" dur="0.4s" to="1"/></path><path d="M14 12c0 0 0 0 0 0c0 0 0 0 0 0Z"><animate fill="freeze" attributeName="d" begin="0.4s" dur="0.2s" to="M14 16c1.5 -0.71 2.5 -2.24 2.5 -4c0 -1.77 -1 -3.26 -2.5 -4Z"/></path><path d="M14 12c0 0 0 0 0 0c0 0 0 0 0 0v0c0 0 0 0 0 0c0 0 0 0 0 0Z"><animate fill="freeze" attributeName="d" begin="0.4s" dur="0.4s" to="M14 3.23c4 0.91 7 4.49 7 8.77c0 4.28 -3 7.86 -7 8.77v-2.07c2.89 -0.86 5 -3.53 5 -6.7c0 -3.17 -2.11 -5.85 -5 -6.71Z"/></path></g>' |
| | | }, |
| | | telegram: { |
| | | body: '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="18" d="M21 5l-2.5 15M21 5l-12 8.5"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.4s" values="18;0"/></path><path stroke-dasharray="24" d="M21 5l-19 7.5"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.4s" values="24;0"/></path><path stroke-dasharray="14" stroke-dashoffset="14" d="M18.5 20l-9.5 -6.5"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.4s" dur="0.3s" to="0"/></path><path stroke-dasharray="10" stroke-dashoffset="10" d="M2 12.5l7 1"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.4s" dur="0.3s" to="0"/></path><path stroke-dasharray="8" stroke-dashoffset="8" d="M12 16l-3 3M9 13.5l0 5.5"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.7s" dur="0.3s" to="0"/></path></g>' |
| | | } |
| | | }, |
| | | prefix: 'line-md', |
| | | width: 24 |
| | | }, |
| | | 'svg-spinners': { |
| | | height: 24, |
| | | icons: { |
| | | '3-dots-bounce': { |
| | | body: '<circle cx="4" cy="12" r="3" fill="currentColor"><animate id="SVGKiXXedfO" attributeName="cy" begin="0;SVGgLulOGrw.end+0.25s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle><circle cx="12" cy="12" r="3" fill="currentColor"><animate attributeName="cy" begin="SVGKiXXedfO.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle><circle cx="20" cy="12" r="3" fill="currentColor"><animate id="SVGgLulOGrw" attributeName="cy" begin="SVGKiXXedfO.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle>' |
| | | }, |
| | | '3-dots-fade': { |
| | | body: '<circle cx="4" cy="12" r="3" fill="currentColor"><animate id="SVG7x14Dcom" fill="freeze" attributeName="opacity" begin="0;SVGqSjG0dUp.end-0.25s" dur="0.75s" values="1;.2"/></circle><circle cx="12" cy="12" r="3" fill="currentColor" opacity=".4"><animate fill="freeze" attributeName="opacity" begin="SVG7x14Dcom.begin+0.15s" dur="0.75s" values="1;.2"/></circle><circle cx="20" cy="12" r="3" fill="currentColor" opacity=".3"><animate id="SVGqSjG0dUp" fill="freeze" attributeName="opacity" begin="SVG7x14Dcom.begin+0.3s" dur="0.75s" values="1;.2"/></circle>' |
| | | }, |
| | | '3-dots-move': { |
| | | body: '<circle cx="4" cy="12" r="0" fill="currentColor"><animate fill="freeze" attributeName="r" begin="0;SVGUppsBdVN.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3"/><animate fill="freeze" attributeName="cx" begin="SVGqCgsydxJ.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12"/><animate fill="freeze" attributeName="cx" begin="SVG3PwDNd6F.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20"/><animate id="SVG3V8yEdYE" fill="freeze" attributeName="r" begin="SVG6wCQhd9Q.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0"/><animate id="SVGUppsBdVN" fill="freeze" attributeName="cx" begin="SVG3V8yEdYE.end" dur="0.001s" values="20;4"/></circle><circle cx="4" cy="12" r="3" fill="currentColor"><animate fill="freeze" attributeName="cx" begin="0;SVGUppsBdVN.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12"/><animate fill="freeze" attributeName="cx" begin="SVGqCgsydxJ.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20"/><animate id="SVG4PgJdbds" fill="freeze" attributeName="r" begin="SVG3PwDNd6F.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0"/><animate id="SVG6wCQhd9Q" fill="freeze" attributeName="cx" begin="SVG4PgJdbds.end" dur="0.001s" values="20;4"/><animate fill="freeze" attributeName="r" begin="SVG6wCQhd9Q.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3"/></circle><circle cx="12" cy="12" r="3" fill="currentColor"><animate fill="freeze" attributeName="cx" begin="0;SVGUppsBdVN.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20"/><animate id="SVG38aCdcdI" fill="freeze" attributeName="r" begin="SVGqCgsydxJ.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0"/><animate id="SVG3PwDNd6F" fill="freeze" attributeName="cx" begin="SVG38aCdcdI.end" dur="0.001s" values="20;4"/><animate fill="freeze" attributeName="r" begin="SVG3PwDNd6F.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3"/><animate fill="freeze" attributeName="cx" begin="SVG6wCQhd9Q.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12"/></circle><circle cx="20" cy="12" r="3" fill="currentColor"><animate id="SVGwaWzveSq" fill="freeze" attributeName="r" begin="0;SVGUppsBdVN.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0"/><animate id="SVGqCgsydxJ" fill="freeze" attributeName="cx" begin="SVGwaWzveSq.end" dur="0.001s" values="20;4"/><animate fill="freeze" attributeName="r" begin="SVGqCgsydxJ.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3"/><animate fill="freeze" attributeName="cx" begin="SVG3PwDNd6F.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12"/><animate fill="freeze" attributeName="cx" begin="SVG6wCQhd9Q.end" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20"/></circle>' |
| | | }, |
| | | '3-dots-rotate': { |
| | | body: '<circle cx="12" cy="12" r="3" fill="currentColor"/><g><circle cx="4" cy="12" r="3" fill="currentColor"/><circle cx="20" cy="12" r="3" fill="currentColor"/><animateTransform attributeName="transform" calcMode="spline" dur="1s" keySplines=".36,.6,.31,1;.36,.6,.31,1" repeatCount="indefinite" type="rotate" values="0 12 12;180 12 12;360 12 12"/></g>' |
| | | }, |
| | | 'blocks-shuffle-2': { |
| | | body: '<rect width="10" height="10" x="1" y="1" fill="currentColor" rx="1"><animate id="SVG7JagGz2Y" fill="freeze" attributeName="x" begin="0;SVGgDT19bUV.end" dur="0.2s" values="1;13"/><animate id="SVGpS1BddYk" fill="freeze" attributeName="y" begin="SVGc7yq8dne.end" dur="0.2s" values="1;13"/><animate id="SVGboa7EdFl" fill="freeze" attributeName="x" begin="SVG0ZX9C6Fa.end" dur="0.2s" values="13;1"/><animate id="SVG6rrusL2C" fill="freeze" attributeName="y" begin="SVGTOnnO5Dr.end" dur="0.2s" values="13;1"/></rect><rect width="10" height="10" x="1" y="13" fill="currentColor" rx="1"><animate id="SVGc7yq8dne" fill="freeze" attributeName="y" begin="SVG7JagGz2Y.end" dur="0.2s" values="13;1"/><animate id="SVG0ZX9C6Fa" fill="freeze" attributeName="x" begin="SVGpS1BddYk.end" dur="0.2s" values="1;13"/><animate id="SVGTOnnO5Dr" fill="freeze" attributeName="y" begin="SVGboa7EdFl.end" dur="0.2s" values="1;13"/><animate id="SVGgDT19bUV" fill="freeze" attributeName="x" begin="SVG6rrusL2C.end" dur="0.2s" values="13;1"/></rect>' |
| | | }, |
| | | 'blocks-wave': { |
| | | body: '<rect width="7.33" height="7.33" x="1" y="1" fill="currentColor"><animate id="SVGzjrPLenI" attributeName="x" begin="0;SVGXAURnSRI.end+0.2s" dur="0.6s" values="1;4;1"/><animate attributeName="y" begin="0;SVGXAURnSRI.end+0.2s" dur="0.6s" values="1;4;1"/><animate attributeName="width" begin="0;SVGXAURnSRI.end+0.2s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="0;SVGXAURnSRI.end+0.2s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="8.33" y="1" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="1;4;1"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="1" y="8.33" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="1;4;1"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.1s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="15.66" y="1" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="1;4;1"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="8.33" y="8.33" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="1" y="15.66" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="1;4;1"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.2s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="15.66" y="8.33" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="8.33" y="15.66" fill="currentColor"><animate attributeName="x" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="8.33;11.33;8.33"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.3s" dur="0.6s" values="7.33;1.33;7.33"/></rect><rect width="7.33" height="7.33" x="15.66" y="15.66" fill="currentColor"><animate id="SVGXAURnSRI" attributeName="x" begin="SVGzjrPLenI.begin+0.4s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="y" begin="SVGzjrPLenI.begin+0.4s" dur="0.6s" values="15.66;18.66;15.66"/><animate attributeName="width" begin="SVGzjrPLenI.begin+0.4s" dur="0.6s" values="7.33;1.33;7.33"/><animate attributeName="height" begin="SVGzjrPLenI.begin+0.4s" dur="0.6s" values="7.33;1.33;7.33"/></rect>' |
| | | }, |
| | | clock: { |
| | | body: '<path fill="currentColor" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,20a9,9,0,1,1,9-9A9,9,0,0,1,12,21Z"/><rect width="2" height="7" x="11" y="6" fill="currentColor" rx="1"><animateTransform attributeName="transform" dur="9s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></rect><rect width="2" height="9" x="11" y="11" fill="currentColor" rx="1"><animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></rect>' |
| | | }, |
| | | tadpole: { |
| | | body: '<path fill="currentColor" d="M12,23a9.63,9.63,0,0,1-8-9.5,9.51,9.51,0,0,1,6.79-9.1A1.66,1.66,0,0,0,12,2.81h0a1.67,1.67,0,0,0-1.94-1.64A11,11,0,0,0,12,23Z"><animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path>' |
| | | } |
| | | }, |
| | | prefix: 'svg-spinners', |
| | | width: 24 |
| | | }, |
| | | 'system-uicons': { |
| | | height: 21, |
| | | icons: { |
| | |
| | | ri: { |
| | | height: 24, |
| | | icons: { |
| | | 'account-box-2-line': { |
| | | body: '<path fill="currentColor" d="M4.995 3A1.995 1.995 0 0 0 3 4.995v14.01C3 20.107 3.893 21 4.995 21h14.01A1.995 1.995 0 0 0 21 19.005V4.995A1.995 1.995 0 0 0 19.005 3zM5 19V5h14v14zm7-11a1 1 0 1 1 0 2a1 1 0 0 1 0-2m0 4a3 3 0 1 0 0-6a3 3 0 0 0 0 6m0 3a2 2 0 0 0-2 2H8a4 4 0 0 1 8 0h-2a2 2 0 0 0-2-2"/>' |
| | | }, |
| | | 'account-circle-line': { |
| | | body: '<path fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2m.16 14a6.98 6.98 0 0 0-5.147 2.256A7.97 7.97 0 0 0 12 20a7.97 7.97 0 0 0 5.167-1.892A6.98 6.98 0 0 0 12.16 16M12 4a8 8 0 0 0-6.384 12.821A8.98 8.98 0 0 1 12.16 14a8.97 8.97 0 0 1 6.362 2.634A8 8 0 0 0 12 4m0 1a4 4 0 1 1 0 8a4 4 0 0 1 0-8m0 2a2 2 0 1 0 0 4a2 2 0 0 0 0-4"/>' |
| | | }, |
| | | 'add-fill': { |
| | | body: '<path fill="currentColor" d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"/>' |
| | | }, |
| | | 'align-item-bottom-line': { |
| | | body: '<path fill="currentColor" d="M9 5v10H6V5zM5 3a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm10 6v6h3V9zm-2-1a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1zm8 11H3v2h18z"/>' |
| | | }, |
| | | 'align-justify': { |
| | | body: '<path fill="currentColor" d="M3 4h18v2H3zm0 15h18v2H3zm0-5h18v2H3zm0-5h18v2H3z"/>' |
| | | }, |
| | | 'align-right': { |
| | | body: '<path fill="currentColor" d="M3 4h18v2H3zm4 15h14v2H7zm-4-5h18v2H3zm4-5h14v2H7z"/>' |
| | | }, |
| | | 'anthropic-line': { |
| | | body: '<path fill="currentColor" d="M14.122 5h2.146L22.1 20h-2.146zM7.66 5h2.681l5.77 15h-2.144l-1.538-4H5.572l-1.539 4H1.891zm4 9L9 7.086L6.341 14z"/>' |
| | | }, |
| | | 'apple-line': { |
| | | body: '<path fill="currentColor" d="M15.778 8.208c-.473-.037-.98.076-1.758.373c.065-.025-.742.29-.969.37c-.502.175-.915.271-1.378.271c-.458 0-.88-.092-1.365-.255a11 11 0 0 1-.505-.186l-.449-.177c-.648-.254-1.012-.35-1.315-.342c-1.153.014-2.243.68-2.877 1.782c-1.292 2.243-.576 6.299 1.313 9.031c1.005 1.444 1.556 1.96 1.777 1.953c.222-.01.386-.057.784-.225l.166-.071c1.006-.429 1.71-.618 2.771-.618c1.021 0 1.703.186 2.669.602l.168.072c.397.17.54.208.792.202c.357-.005.798-.417 1.777-1.854c.268-.391.505-.803.71-1.22a7 7 0 0 1-.391-.347c-1.29-1.228-2.087-2.884-2.109-4.93A6.63 6.63 0 0 1 17 8.458a4.1 4.1 0 0 0-1.221-.25m.155-1.994c.708.048 2.736.264 4.056 2.196c-.108.06-2.424 1.404-2.4 4.212c.036 3.36 2.94 4.476 2.976 4.488c-.024.084-.468 1.596-1.536 3.156c-.924 1.356-1.884 2.7-3.396 2.724c-1.488.036-1.968-.876-3.66-.876c-1.704 0-2.232.852-3.636.912c-1.464.048-2.568-1.464-3.504-2.808c-1.908-2.76-3.36-7.776-1.404-11.172c.972-1.692 2.7-2.76 4.584-2.784c1.428-.036 2.784.96 3.66.96c.864 0 2.412-1.152 4.26-1.008m-1.14-1.824c-.78.936-2.052 1.668-3.288 1.572c-.168-1.272.456-2.604 1.176-3.432c.804-.936 2.148-1.632 3.264-1.68c.144 1.296-.372 2.604-1.152 3.54"/>' |
| | | }, |
| | | 'apps-2-add-line': { |
| | | body: '<path fill="currentColor" d="M2.5 7a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0m0 10a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0m10 0a4.5 4.5 0 1 0 9 0a4.5 4.5 0 0 0-9 0m-3-10a2.5 2.5 0 1 1-5 0a2.5 2.5 0 0 1 5 0m0 10a2.5 2.5 0 1 1-5 0a2.5 2.5 0 0 1 5 0m10 0a2.5 2.5 0 1 1-5 0a2.5 2.5 0 0 1 5 0M16 11V8h-3V6h3V3h2v3h3v2h-3v3z"/>' |
| | | }, |
| | | 'apps-2-line': { |
| | | body: '<path fill="currentColor" d="M7 11.5a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9m0 10a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9m10-10a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9m0 10a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9M7 9.5a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5m0 10a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5m10-10a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5m0 10a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5"/>' |
| | | 'archive-line': { |
| | | body: '<path fill="currentColor" d="M3 10H2V4.003C2 3.449 2.455 3 2.992 3h18.016A.99.99 0 0 1 22 4.003V10h-1v10.002a.996.996 0 0 1-.993.998H3.993A.996.996 0 0 1 3 20.002zm16 0H5v9h14zM4 5v3h16V5zm5 7h6v2H9z"/>' |
| | | }, |
| | | 'arrow-down-wide-fill': { |
| | | body: '<path fill="currentColor" d="m12 15.632l8.968-4.748l-.936-1.768L12 13.368L3.968 9.116l-.936 1.768z"/>' |
| | |
| | | 'arrow-left-s-line': { |
| | | body: '<path fill="currentColor" d="m10.828 12l4.95 4.95l-1.414 1.415L8 12l6.364-6.364l1.414 1.414z"/>' |
| | | }, |
| | | 'arrow-left-wide-fill': { |
| | | body: '<path fill="currentColor" d="m8.369 12l4.747-8.968l1.768.936L10.632 12l4.252 8.032l-1.768.936z"/>' |
| | | }, |
| | | 'arrow-right-circle-line': { |
| | | body: '<path fill="currentColor" d="M12 11V8l4 4l-4 4v-3H8v-2zm0-9c5.52 0 10 4.48 10 10s-4.48 10-10 10S2 17.52 2 12S6.48 2 12 2m0 18c4.42 0 8-3.58 8-8s-3.58-8-8-8s-8 3.58-8 8s3.58 8 8 8"/>' |
| | | }, |
| | | 'arrow-right-s-line': { |
| | | body: '<path fill="currentColor" d="m13.172 12l-4.95-4.95l1.414-1.413L16 12l-6.364 6.364l-1.414-1.415z"/>' |
| | | }, |
| | | 'arrow-right-up-line': { |
| | | body: '<path fill="currentColor" d="m16.004 9.414l-8.607 8.607l-1.414-1.414L14.59 8H7.003V6h11v11h-2z"/>' |
| | | }, |
| | | 'arrow-up-circle-line': { |
| | | body: '<path fill="currentColor" d="M12 2c5.52 0 10 4.48 10 10s-4.48 10-10 10S2 17.52 2 12S6.48 2 12 2m0 18c4.42 0 8-3.58 8-8s-3.58-8-8-8s-8 3.58-8 8s3.58 8 8 8m1-8v4h-2v-4H8l4-4l4 4z"/>' |
| | | 'arrow-right-wide-fill': { |
| | | body: '<path fill="currentColor" d="m15.632 12l-4.748-8.968l-1.768.936L13.368 12l-4.252 8.032l1.768.936z"/>' |
| | | }, |
| | | 'arrow-up-down-fill': { |
| | | body: '<path fill="currentColor" d="M12 8H8.001L8 20H6V8H2l5-5zm10 8l-5 5l-5-5h4V4h2v12z"/>' |
| | | }, |
| | | 'arrow-up-line': { |
| | | body: '<path fill="currentColor" d="M13 7.828V20h-2V7.828l-5.364 5.364l-1.414-1.414L12 4l7.778 7.778l-1.414 1.414z"/>' |
| | | }, |
| | | 'arrow-up-wide-fill': { |
| | | body: '<path fill="currentColor" d="m12 8.369l8.968 4.747l-.936 1.768L12 10.632l-8.032 4.252l-.936-1.768z"/>' |
| | |
| | | 'arrow-up-wide-line': { |
| | | body: '<path fill="currentColor" d="m12 8.369l8.968 4.747l-.936 1.768L12 10.632l-8.032 4.252l-.936-1.768z"/>' |
| | | }, |
| | | 'article-line': { |
| | | body: '<path fill="currentColor" d="M20 22H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1m-1-2V4H5v16zM7 6h4v4H7zm0 6h10v2H7zm0 4h10v2H7zm6-9h4v2h-4z"/>' |
| | | }, |
| | | 'bar-chart-2-line': { |
| | | body: '<path fill="currentColor" d="M2 13h6v8H2zm14-5h6v13h-6zM9 3h6v18H9zM4 15v4h2v-4zm7-10v14h2V5zm7 5v9h2v-9z"/>' |
| | | }, |
| | | 'bar-chart-box-ai-line': { |
| | | body: '<path fill="currentColor" d="m20.713 8.128l-.246.566a.506.506 0 0 1-.934 0l-.246-.566a4.36 4.36 0 0 0-2.22-2.25l-.759-.339a.53.53 0 0 1 0-.963l.717-.319a4.37 4.37 0 0 0 2.251-2.326l.253-.611a.506.506 0 0 1 .942 0l.253.61a4.37 4.37 0 0 0 2.25 2.327l.718.32a.53.53 0 0 1 0 .962l-.76.338a4.36 4.36 0 0 0-2.219 2.251M2 4a1 1 0 0 1 1-1h11v2H4v14h16v-8h2v9a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1zm5 9h2v4H7zm4-6h2v10h-2zm4 3h2v7h-2z"/>' |
| | | }, |
| | | 'bar-chart-box-line': { |
| | | body: '<path fill="currentColor" d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m1 2v14h16V5zm3 8h2v4H7zm4-6h2v10h-2zm4 3h2v7h-2z"/>' |
| | | }, |
| | | 'bar-chart-grouped-line': { |
| | | body: '<path fill="currentColor" d="M2 12h2v9H2zm3 2h2v7H5zm11-6h2v13h-2zm3 2h2v11h-2zM9 2h2v19H9zm3 2h2v17h-2z"/>' |
| | | }, |
| | | 'bilibili-line': { |
| | | body: '<path fill="currentColor" d="M7.172 2.757L10.414 6h3.171l3.243-3.242a1 1 0 1 1 1.415 1.415L16.414 6H18.5A3.5 3.5 0 0 1 22 9.5v8a3.5 3.5 0 0 1-3.5 3.5h-13A3.5 3.5 0 0 1 2 17.5v-8A3.5 3.5 0 0 1 5.5 6h2.085L5.757 4.171a1 1 0 0 1 1.415-1.415M18.5 8h-13a1.5 1.5 0 0 0-1.493 1.356L4 9.5v8a1.5 1.5 0 0 0 1.356 1.493L5.5 19h13a1.5 1.5 0 0 0 1.493-1.355L20 17.5v-8A1.5 1.5 0 0 0 18.5 8M8 11a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1m8 0a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1"/>' |
| | |
| | | 'book-2-line': { |
| | | body: '<path fill="currentColor" d="M21 18H6a1 1 0 1 0 0 2h15v2H6a3 3 0 0 1-3-3V4a2 2 0 0 1 2-2h16zM5 16.05q.243-.05.5-.05H19V4H5zM16 9H8V7h8z"/>' |
| | | }, |
| | | 'box-1-line': { |
| | | body: '<path fill="currentColor" d="m12 1l9.5 5.5v11L12 23l-9.5-5.5v-11zM5.494 7.078L13 11.423v8.687l6.5-3.763V7.653L12 3.311zM4.5 8.813v7.534L11 20.11v-7.533z"/>' |
| | | }, |
| | | 'bus-2-line': { |
| | | body: '<path fill="currentColor" d="M17 20H7v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9H2V8h1V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v3h1v4h-1v9a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1zM5 5v6h14V5zm14 8H5v5h14zM7.5 17a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m9 0a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3"/>' |
| | | }, |
| | | 'calendar-2-line': { |
| | | body: '<path fill="currentColor" d="M9 1v2h6V1h2v2h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h4V1zm11 10H4v8h16zM8 13v2H6v-2zm5 0v2h-2v-2zm5 0v2h-2v-2zM7 5H4v4h16V5h-3v2h-2V5H9v2H7z"/>' |
| | | }, |
| | | 'camera-4-line': { |
| | | body: '<path fill="currentColor" d="M14.434 3a2 2 0 0 1 1.714.97l.773 1.287a.5.5 0 0 0 .429.243H19a3 3 0 0 1 3 3V18a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V8.5a3 3 0 0 1 3-3h1.65a.5.5 0 0 0 .43-.243l.772-1.286A2 2 0 0 1 9.566 3zm-5.64 3.286A2.5 2.5 0 0 1 6.65 7.5H5a1 1 0 0 0-1 1V18a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V8.5a1 1 0 0 0-1-1h-1.65a2.5 2.5 0 0 1-2.145-1.214L14.434 5H9.566zM12 8.5a4.5 4.5 0 1 1 0 9a4.5 4.5 0 0 1 0-9m0 2a2.5 2.5 0 1 0 0 5a2.5 2.5 0 0 0 0-5"/>' |
| | | }, |
| | | 'capsule-line': { |
| | | body: '<path fill="currentColor" d="M19.779 4.222a6 6 0 0 1 0 8.485l-7.071 7.071a6 6 0 0 1-8.486-8.485l7.071-7.071a6 6 0 0 1 8.486 0m-5.657 11.313L8.466 9.878l-2.83 2.83a4 4 0 0 0 5.657 5.656zm4.242-9.899a4 4 0 0 0-5.657 0L9.88 8.464l5.657 5.657l2.827-2.828a4 4 0 0 0 0-5.657"/>' |
| | | }, |
| | | 'check-fill': { |
| | | body: '<path fill="currentColor" d="m10 15.17l9.192-9.191l1.414 1.414L10 17.999l-6.364-6.364l1.414-1.414z"/>' |
| | | }, |
| | | 'checkbox-blank-circle-line': { |
| | | body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16"/>' |
| | | }, |
| | | 'checkbox-circle-line': { |
| | | body: '<path fill="currentColor" d="M4 12a8 8 0 1 1 16 0a8 8 0 0 1-16 0m8-10C6.477 2 2 6.477 2 12s4.477 10 10 10s10-4.477 10-10S17.523 2 12 2m5.457 7.457l-1.414-1.414L11 13.086l-2.793-2.793l-1.414 1.414L11 15.914z"/>' |
| | | }, |
| | | 'clipboard-line': { |
| | | body: '<path fill="currentColor" d="M7 4V2h10v2h3.007c.548 0 .993.445.993.993v16.014a.994.994 0 0 1-.993.993H3.993A.993.993 0 0 1 3 21.007V4.993C3 4.445 3.445 4 3.993 4zm0 2H5v14h14V6h-2v2H7zm2-2v2h6V4z"/>' |
| | | }, |
| | | 'close-circle-line': { |
| | | body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m0-9.414l2.828-2.829l1.415 1.415L13.414 12l2.829 2.828l-1.415 1.415L12 13.414l-2.828 2.829l-1.415-1.415L10.586 12L7.757 9.172l1.415-1.415z"/>' |
| | |
| | | 'command-fill': { |
| | | body: '<path fill="currentColor" d="M10 8h4V6.5a3.5 3.5 0 1 1 3.5 3.5H16v4h1.5a3.5 3.5 0 1 1-3.5 3.5V16h-4v1.5A3.5 3.5 0 1 1 6.5 14H8v-4H6.5A3.5 3.5 0 1 1 10 6.5zM8 8V6.5A1.5 1.5 0 1 0 6.5 8zm0 8H6.5A1.5 1.5 0 1 0 8 17.5zm8-8h1.5A1.5 1.5 0 1 0 16 6.5zm0 8v1.5a1.5 1.5 0 1 0 1.5-1.5zm-6-6v4h4v-4z"/>' |
| | | }, |
| | | 'contacts-line': { |
| | | body: '<path fill="currentColor" d="M19 7h5v2h-5zm-2 5h7v2h-7zm3 5h4v2h-4zM2 22a8 8 0 1 1 16 0h-2a6 6 0 0 0-12 0zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4"/>' |
| | | }, |
| | | 'copilot-line': { |
| | | body: '<path fill="currentColor" d="M5.4 7.8c0-2.088 1.178-3 3.172-3c1.196 0 2.129.264 2.129 1.6c0 1.814-.575 3.75-2.7 3.75c-1.229 0-1.798-.176-2.09-.424c-.247-.21-.51-.67-.51-1.926m3.172-5C5.497 2.8 3.4 4.626 3.4 7.8c0 .999.137 1.89.53 2.605l-.183.364a6.3 6.3 0 0 0-1.425 1.107c-1.061 1.126-.973 2.389-.973 3.824c0 2.267 2.512 3.62 4.315 4.373c2.133.89 4.677 1.427 6.336 1.427c1.658 0 4.202-.537 6.335-1.427c1.803-.753 4.315-2.106 4.315-4.373c0-1.435.088-2.698-.973-3.824a6.3 6.3 0 0 0-1.425-1.107l-.182-.364c.392-.716.53-1.606.53-2.605c0-3.174-2.097-5-5.172-5c-1.24 0-2.618.259-3.428 1.283C11.19 3.059 9.813 2.8 8.57 2.8M8 12.15c1.692 0 3.224-.815 4-2.334c.775 1.519 2.307 2.334 4 2.334c.894 0 1.769-.074 2.517-.38c.511.596 1.17.911 1.705 1.478c.639.678.428 1.585.428 2.452c0 1.272-2.166 2.143-3.086 2.527c-1.942.81-4.223 1.273-5.565 1.273c-1.341 0-3.623-.463-5.565-1.273c-.919-.384-3.085-1.255-3.085-2.527c0-.867-.21-1.774.428-2.452c.56-.594 1.341-.75 1.705-1.478c.748.306 1.623.38 2.518.38m5.3-5.75c0-1.336.932-1.6 2.128-1.6c1.994 0 3.172.912 3.172 3c0 1.257-.264 1.715-.511 1.926c-.292.248-.861.424-2.09.424c-2.125 0-2.7-1.936-2.7-3.75m-4.638 8.084a1.001 1.001 0 1 1 2.002 0v1.997a1.001 1.001 0 1 1-2.002 0zm6.675 0a1.001 1.001 0 1 0-2.003 0v1.997a1.001 1.001 0 1 0 2.003 0z"/>' |
| | | }, |
| | | 'customer-service-2-line': { |
| | | body: '<path fill="currentColor" d="M19.938 8H21a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-1.062A8 8 0 0 1 12 23v-2a6 6 0 0 0 6-6V9A6 6 0 0 0 6 9v7H3a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1.062a8.001 8.001 0 0 1 15.876 0M3 10v4h1v-4zm17 0v4h1v-4zM7.76 15.785l1.06-1.696A5.97 5.97 0 0 0 12 15a5.97 5.97 0 0 0 3.18-.911l1.06 1.696A7.96 7.96 0 0 1 12 17a7.96 7.96 0 0 1-4.24-1.215"/>' |
| | | 'computer-line': { |
| | | body: '<path fill="currentColor" d="M4 16h16V5H4zm9 2v2h4v2H7v-2h4v-2H2.992A1 1 0 0 1 2 16.992V4.008C2 3.451 2.455 3 2.992 3h18.016c.548 0 .992.449.992 1.007v12.985c0 .557-.455 1.008-.992 1.008z"/>' |
| | | }, |
| | | 'delete-bin-4-line': { |
| | | body: '<path fill="currentColor" d="M20 7v14a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7H2V5h20v2zM6 7v13h12V7zm1-5h10v2H7zm4 8h2v7h-2z"/>' |
| | |
| | | 'delete-bin-5-line': { |
| | | body: '<path fill="currentColor" d="M4 8h16v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm2 2v10h12V10zm3 2h2v6H9zm4 0h2v6h-2zM7 5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v2h5v2H2V5zm2-1v1h6V4z"/>' |
| | | }, |
| | | 'delete-bin-line': { |
| | | body: '<path fill="currentColor" d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zm1 2H6v12h12zm-9 3h2v6H9zm4 0h2v6h-2zM9 4v2h6V4z"/>' |
| | | }, |
| | | 'download-2-line': { |
| | | body: '<path fill="currentColor" d="M13 10h5l-6 6l-6-6h5V3h2zm-9 9h16v-7h2v8a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-8h2z"/>' |
| | | }, |
| | | 'drag-move-fill': { |
| | | body: '<path fill="currentColor" d="m12 22l-4-4h8zm0-20l4 4H8zm0 12a2 2 0 1 1 0-4a2 2 0 0 1 0 4M2 12l4-4v8zm20 0l-4 4V8z"/>' |
| | | 'drag-move-2-fill': { |
| | | body: '<path fill="currentColor" d="M18 11V8l4 4l-4 4v-3h-5v5h3l-4 4l-4-4h3v-5H6v3l-4-4l4-4v3h5V6H8l4-4l4 4h-3v5z"/>' |
| | | }, |
| | | 'dribbble-fill': { |
| | | body: '<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c5.51 0 10-4.48 10-10S17.51 2 12 2m6.605 4.61a8.5 8.5 0 0 1 1.93 5.314c-.281-.054-3.101-.629-5.943-.271c-.065-.141-.12-.293-.184-.445a25 25 0 0 0-.564-1.236c3.145-1.28 4.577-3.124 4.761-3.362M12 3.475c2.17 0 4.154.814 5.662 2.148c-.152.216-1.443 1.941-4.48 3.08c-1.399-2.57-2.95-4.675-3.189-5A8.7 8.7 0 0 1 12 3.475m-3.633.803a54 54 0 0 1 3.167 4.935c-3.992 1.063-7.517 1.04-7.896 1.04a8.58 8.58 0 0 1 4.729-5.975M3.453 12.01v-.26c.37.01 4.512.065 8.775-1.215c.25.477.477.965.694 1.453c-.109.033-.228.065-.336.098c-4.404 1.42-6.747 5.303-6.942 5.629a8.52 8.52 0 0 1-2.19-5.705M12 20.547a8.48 8.48 0 0 1-5.239-1.8c.152-.315 1.888-3.656 6.703-5.337c.022-.01.033-.01.054-.022a35.3 35.3 0 0 1 1.823 6.475a8.4 8.4 0 0 1-3.341.684m4.761-1.465c-.086-.52-.542-3.015-1.66-6.084c2.68-.423 5.023.271 5.315.369a8.47 8.47 0 0 1-3.655 5.715"/>' |
| | | }, |
| | | 'edge-line': { |
| | | body: '<path fill="currentColor" d="M8.008 14.001A5 5 0 0 0 8 14.25C8 16.632 9.753 19 13 19c2.373 0 4.528-.655 6-1.553v3.35C17.211 21.564 15.112 22 13 22c-5.502 0-8-3.47-8-7.75c0-3.231 2.041-6 4.943-7.164C8.54 8.663 8 10.341 8 10.996L18 11c0-3.406-2.548-6-6-6c-5 0-8.001 3.988-9 5.999C3.29 6.237 7.01 2 12 2c5.2 0 9 4.03 9 9v3H8z"/>' |
| | | }, |
| | | 'edit-2-line': { |
| | | body: '<path fill="currentColor" d="M5 18.89h1.414l9.314-9.314l-1.414-1.414L5 17.476zm16 2H3v-4.243L16.435 3.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414L9.243 18.89H21zM15.728 6.748l1.414 1.414l1.414-1.414l-1.414-1.414z"/>' |
| | |
| | | 'error-warning-line': { |
| | | body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m-1-5h2v2h-2zm0-8h2v6h-2z"/>' |
| | | }, |
| | | 'export-line': { |
| | | body: '<path fill="currentColor" d="M22 4a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1zM4 15h3.416a5.001 5.001 0 0 0 9.168 0H20v4H4zM4 5h16v8h-5a3 3 0 1 1-6 0H4zm12 6h-3v3h-2v-3H8l4-4.5z"/>' |
| | | }, |
| | | 'eye-line': { |
| | | body: '<path fill="currentColor" d="M12 3c5.392 0 9.878 3.88 10.819 9c-.94 5.12-5.427 9-10.819 9s-9.878-3.88-10.818-9C2.122 6.88 6.608 3 12 3m0 16a9.005 9.005 0 0 0 8.778-7a9.005 9.005 0 0 0-17.555 0A9.005 9.005 0 0 0 12 19m0-2.5a4.5 4.5 0 1 1 0-9a4.5 4.5 0 0 1 0 9m0-2a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5"/>' |
| | | }, |
| | | 'file-copy-line': { |
| | | body: '<path fill="currentColor" d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1 1 0 0 1 3 21l.003-14c0-.552.45-1 1.006-1zM5.002 8L5 20h10V8zM9 6h8v10h2V4H9z"/>' |
| | | }, |
| | | 'file-excel-2-line': { |
| | | body: '<path fill="currentColor" d="m2.859 2.877l12.57-1.795a.5.5 0 0 1 .571.494v20.848a.5.5 0 0 1-.57.494L2.858 21.123a1 1 0 0 1-.859-.99V3.867a1 1 0 0 1 .859-.99M4 4.735v14.53l10 1.429V3.306zM17 19h3V5h-3V3h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4zm-6.8-7l2.8 4h-2.4L9 13.714L7.4 16H5l2.8-4L5 8h2.4L9 10.286L10.6 8H13z"/>' |
| | | }, |
| | | 'file-pdf-2-line': { |
| | | body: '<path fill="currentColor" d="M5 4h10v4h4v12H5zM3.999 2A.995.995 0 0 0 3 2.992v18.016a1 1 0 0 0 .993.992h16.014A1 1 0 0 0 21 20.992V7l-5-5zm6.5 5.5c0 1.577-.455 3.437-1.224 5.153c-.772 1.723-1.814 3.197-2.9 4.066l1.18 1.613c2.927-1.952 6.168-3.29 9.304-2.842l.457-1.939C14.644 12.661 12.5 9.99 12.5 7.5zm.6 5.972c.268-.597.505-1.216.705-1.843a9.7 9.7 0 0 0 1.706 1.966c-.982.176-1.944.465-2.875.833q.248-.471.465-.956"/>' |
| | | }, |
| | | 'fingerprint-line': { |
| | | body: '<path fill="currentColor" d="M17 13v1c0 2.77-.664 5.445-1.915 7.846l-.227.42l-1.746-.974a14.9 14.9 0 0 0 1.881-6.836L15 14v-1zm-6-3h2v4l-.005.379a12.94 12.94 0 0 1-2.691 7.549l-.231.29l-1.549-1.264a10.94 10.94 0 0 0 2.47-6.588L11 14zm1-4a5 5 0 0 1 5 5h-2a3 3 0 0 0-6 0v3c0 2.235-.82 4.344-2.27 5.977l-.212.23l-1.448-1.38a6.97 6.97 0 0 0 1.924-4.524L7 14v-3a5 5 0 0 1 5-5m0-4a9 9 0 0 1 9 9v3c0 1.698-.201 3.37-.596 4.99l-.14.539l-1.93-.526c.392-1.437.614-2.922.658-4.435L19 14v-3A7 7 0 0 0 7.808 5.394L6.383 3.968A8.96 8.96 0 0 1 12 2M4.968 5.383l1.426 1.425a6.97 6.97 0 0 0-1.39 3.951L5 11l.004 2c0 1.12-.264 2.203-.761 3.177l-.157.29l-1.736-.992c.379-.665.6-1.407.645-2.183L3.004 13v-2a8.94 8.94 0 0 1 1.964-5.617"/>' |
| | | 'file-list-3-line': { |
| | | body: '<path fill="currentColor" d="M19 22H5a3 3 0 0 1-3-3V3a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v12h4v4a3 3 0 0 1-3 3m-1-5v2a1 1 0 1 0 2 0v-2zm-2 3V4H4v15a1 1 0 0 0 1 1zM6 7h8v2H6zm0 4h8v2H6zm0 4h5v2H6z"/>' |
| | | }, |
| | | 'fire-line': { |
| | | body: '<path fill="currentColor" d="M12 23a7.5 7.5 0 0 0 7.5-7.5c0-.866-.23-1.697-.5-2.47q-2.5 2.47-3.8 2.47c3.995-7 1.8-10-4.2-14c.5 5-2.796 7.274-4.138 8.537A7.5 7.5 0 0 0 12 23m.71-17.765c3.241 2.75 3.257 4.887.753 9.274c-.761 1.333.202 2.991 1.737 2.991c.688 0 1.384-.2 2.119-.595a5.5 5.5 0 1 1-9.087-5.412c.126-.118.765-.685.793-.71c.424-.38.773-.717 1.118-1.086c1.23-1.318 2.114-2.78 2.566-4.462"/>' |
| | | }, |
| | | 'flag-2-line': { |
| | | body: '<path fill="currentColor" d="M21.138 3a.5.5 0 0 1 .434.748L18 10l3.573 6.252a.5.5 0 0 1-.435.748H4v5H2V3zm-2.584 2H4v10h14.554l-2.857-5z"/>' |
| | | 'fullscreen-exit-line': { |
| | | body: '<path fill="currentColor" d="M18 7h4v2h-6V3h2zM8 9H2V7h4V3h2zm10 8v4h-2v-6h6v2zM8 15v6H6v-4H2v-2z"/>' |
| | | }, |
| | | 'fullscreen-fill': { |
| | | body: '<path fill="currentColor" d="M16 3h6v6h-2V5h-4zM2 3h6v2H4v4H2zm18 16v-4h2v6h-6v-2zM4 19h4v2H2v-6h2z"/>' |
| | | }, |
| | | 'fullscreen-line': { |
| | | body: '<path fill="currentColor" d="M8 3v2H4v4H2V3zM2 21v-6h2v4h4v2zm20 0h-6v-2h4v-4h2zm0-12h-2V5h-4V3h6z"/>' |
| | | }, |
| | | 'function-line': { |
| | | body: '<path fill="currentColor" d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zm0 10a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zM13 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm0 10a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm2-9v4h4V5zm0 10v4h4v-4zM5 5v4h4V5zm0 10v4h4v-4z"/>' |
| | | }, |
| | | 'game-line': { |
| | | body: '<path fill="currentColor" d="M12 2a9.98 9.98 0 0 1 7.743 3.671L13.414 12l6.329 6.329A9.98 9.98 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2m0 2a8 8 0 1 0 4.697 14.477l.208-.157l-6.32-6.32l6.32-6.321l-.208-.156a7.97 7.97 0 0 0-4.394-1.517zm0 1a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3"/>' |
| | | }, |
| | | 'gamepad-line': { |
| | | body: '<path fill="currentColor" d="M17 4a6 6 0 0 1 6 6v4a6 6 0 0 1-6 6H7a6 6 0 0 1-6-6v-4a6 6 0 0 1 6-6zm0 2H7a4 4 0 0 0-3.995 3.8L3 10v4a4 4 0 0 0 3.8 3.995L7 18h10a4 4 0 0 0 3.995-3.8L21 14v-4a4 4 0 0 0-3.8-3.995zm-7 3v2h2v2H9.999L10 15H8l-.001-2H6v-2h2V9zm8 4v2h-2v-2zm-2-4v2h-2V9z"/>' |
| | | }, |
| | | 'gift-2-line': { |
| | | body: '<path fill="currentColor" d="M14.505 2.003a3.5 3.5 0 0 1 3.163 5h3.337a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-1v8a1 1 0 0 1-1 1h-14a1 1 0 0 1-1-1v-8h-1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h3.337a3.5 3.5 0 0 1 5.664-3.95a3.48 3.48 0 0 1 2.499-1.05m3.5 11h-12v7h12zm2-4h-16v2h16zm-10.5-5a1.5 1.5 0 0 0-.145 2.993l.145.007h1.5v-1.5A1.5 1.5 0 0 0 9.649 4.01zm5 0l-.145.007a1.5 1.5 0 0 0-1.348 1.348l-.007.145v1.5h1.5l.144-.007a1.5 1.5 0 0 0 0-2.986z"/>' |
| | | }, |
| | | 'github-fill': { |
| | | body: '<path fill="currentColor" d="M12.001 2c-5.525 0-10 4.475-10 10a9.99 9.99 0 0 0 6.837 9.488c.5.087.688-.213.688-.476c0-.237-.013-1.024-.013-1.862c-2.512.463-3.162-.612-3.362-1.175c-.113-.288-.6-1.175-1.025-1.413c-.35-.187-.85-.65-.013-.662c.788-.013 1.35.725 1.538 1.025c.9 1.512 2.337 1.087 2.912.825c.088-.65.35-1.087.638-1.337c-2.225-.25-4.55-1.113-4.55-4.938c0-1.088.387-1.987 1.025-2.687c-.1-.25-.45-1.275.1-2.65c0 0 .837-.263 2.75 1.024a9.3 9.3 0 0 1 2.5-.337c.85 0 1.7.112 2.5.337c1.913-1.3 2.75-1.024 2.75-1.024c.55 1.375.2 2.4.1 2.65c.637.7 1.025 1.587 1.025 2.687c0 3.838-2.337 4.688-4.562 4.938c.362.312.675.912.675 1.85c0 1.337-.013 2.412-.013 2.75c0 .262.188.574.688.474A10.02 10.02 0 0 0 22 12c0-5.525-4.475-10-10-10"/>' |
| | | }, |
| | | 'github-line': { |
| | | body: '<path fill="currentColor" d="M5.884 18.653c-.3-.2-.558-.455-.86-.816a51 51 0 0 1-.466-.579c-.463-.575-.755-.841-1.056-.95a1 1 0 1 1 .675-1.882c.752.27 1.261.735 1.947 1.588c-.094-.117.34.427.433.539c.19.227.33.365.44.438c.204.137.588.196 1.15.14c.024-.382.094-.753.202-1.095c-2.968-.726-4.648-2.64-4.648-6.396c0-1.24.37-2.356 1.058-3.292c-.218-.894-.185-1.975.302-3.192a1 1 0 0 1 .63-.582c.081-.024.127-.035.208-.047c.803-.124 1.937.17 3.415 1.096a11.7 11.7 0 0 1 2.687-.308c.912 0 1.819.104 2.684.308c1.477-.933 2.614-1.227 3.422-1.096q.128.02.218.05a1 1 0 0 1 .616.58c.487 1.216.52 2.296.302 3.19c.691.936 1.058 2.045 1.058 3.293c0 3.757-1.674 5.665-4.642 6.392c.125.415.19.878.19 1.38c0 .665-.002 1.299-.007 2.01c0 .19-.002.394-.005.706a1 1 0 0 1-.018 1.958c-1.14.227-1.984-.532-1.984-1.525l.002-.447l.005-.705c.005-.707.008-1.337.008-1.997c0-.697-.184-1.152-.426-1.361c-.661-.57-.326-1.654.541-1.751c2.966-.333 4.336-1.482 4.336-4.66c0-.955-.312-1.744-.913-2.404A1 1 0 0 1 17.2 6.19c.166-.414.236-.957.095-1.614l-.01.003c-.491.139-1.11.44-1.858.949a1 1 0 0 1-.833.135a9.6 9.6 0 0 0-2.592-.349c-.89 0-1.772.118-2.592.35a1 1 0 0 1-.829-.134c-.753-.507-1.374-.807-1.87-.947c-.143.653-.072 1.194.093 1.607a1 1 0 0 1-.189 1.045c-.597.655-.913 1.458-.913 2.404c0 3.172 1.371 4.328 4.322 4.66c.865.097 1.202 1.177.545 1.748c-.193.168-.43.732-.43 1.364v3.15c0 .985-.834 1.725-1.96 1.528a1 1 0 0 1-.04-1.962v-.99c-.91.061-1.661-.088-2.254-.485"/>' |
| | |
| | | 'group-line': { |
| | | body: '<path fill="currentColor" d="M2 22a8 8 0 1 1 16 0h-2a6 6 0 0 0-12 0zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4m8.284 3.703A8 8 0 0 1 23 22h-2a6 6 0 0 0-3.537-5.473zm-.688-11.29A5.5 5.5 0 0 1 21 8.5a5.5 5.5 0 0 1-5 5.478v-2.013a3.5 3.5 0 0 0 1.041-6.609z"/>' |
| | | }, |
| | | 'hard-drive-3-line': { |
| | | body: '<path fill="currentColor" d="M4.508 2.876A1 1 0 0 1 5.5 2h13a1 1 0 0 1 .992.876l1.5 12Q21 14.938 21 15v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-6a1 1 0 0 1 .008-.124zM6.383 4l-1.25 10h13.734l-1.25-10zM19 16H5v4h14zm-4 1h2v2h-2zm-2 0h-2v2h2z"/>' |
| | | }, |
| | | 'heart-3-line': { |
| | | body: '<path fill="currentColor" d="M16.5 3C19.538 3 22 5.5 22 9c0 7-7.5 11-10 12.5C9.5 20 2 16 2 9c0-3.5 2.5-6 5.5-6C9.36 3 11 4 12 5c1-1 2.64-2 4.5-2m-3.566 15.604a27 27 0 0 0 2.42-1.701C18.335 14.533 20 11.943 20 9c0-2.36-1.537-4-3.5-4c-1.076 0-2.24.57-3.086 1.414L12 7.828l-1.414-1.414C9.74 5.57 8.576 5 7.5 5C5.56 5 4 6.657 4 9c0 2.944 1.666 5.533 4.645 7.903c.745.593 1.54 1.146 2.421 1.7c.299.189.595.37.934.572c.339-.202.635-.383.934-.571"/>' |
| | | }, |
| | | 'heart-fill': { |
| | | body: '<path fill="currentColor" d="M12.001 4.529a6 6 0 0 1 8.242.228a6 6 0 0 1 .236 8.236l-8.48 8.492l-8.478-8.492a6 6 0 0 1 8.48-8.464"/>' |
| | | }, |
| | | 'heart-line': { |
| | | body: '<path fill="currentColor" d="M12.001 4.529a6 6 0 0 1 8.242.228a6 6 0 0 1 .236 8.236l-8.48 8.492l-8.478-8.492a6 6 0 0 1 8.48-8.464m6.826 1.641a4 4 0 0 0-5.49-.153l-1.335 1.198l-1.336-1.197a4 4 0 0 0-5.686 5.605L12 18.654l7.02-7.03a4 4 0 0 0-.193-5.454"/>' |
| | | }, |
| | | 'home-line': { |
| | | body: '<path fill="currentColor" d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.49a1 1 0 0 1 .386-.79l8-6.223a1 1 0 0 1 1.228 0l8 6.223a1 1 0 0 1 .386.79zm-2-1V9.978l-7-5.444l-7 5.444V19z"/>' |
| | | 'history-line': { |
| | | body: '<path fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12h2a8 8 0 1 0 1.385-4.5H8v2H2v-6h2V6a9.99 9.99 0 0 1 8-4m1 5v4.585l3.243 3.243l-1.415 1.415L11 12.413V7z"/>' |
| | | }, |
| | | 'home-smile-2-line': { |
| | | body: '<path fill="currentColor" d="M19 19V9.799l-7-5.522l-7 5.522V19zm2 1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9.314a1 1 0 0 1 .38-.785l8-6.311a1 1 0 0 1 1.24 0l8 6.31a1 1 0 0 1 .38.786zM7 12h2a3 3 0 1 0 6 0h2a5 5 0 0 1-10 0"/>' |
| | |
| | | 'image-line': { |
| | | body: '<path fill="currentColor" d="M2.992 21A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993zM20 15V5H4v14L14 9zm0 2.828l-6-6L6.828 19H20zM8 11a2 2 0 1 1 0-4a2 2 0 0 1 0 4"/>' |
| | | }, |
| | | 'input-method-line': { |
| | | body: '<path fill="currentColor" d="M5 5v14h14V5zM4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m5.869 12l-.82 2H6.833L11 7h2l4.167 10H14.95l-.82-2zm.82-2h2.623L12 9.8z"/>' |
| | | 'key-2-line': { |
| | | body: '<path fill="currentColor" d="m10.758 11.828l7.849-7.849l1.414 1.414l-1.414 1.415l2.474 2.474l-1.414 1.415l-2.475-2.475l-1.414 1.414l2.121 2.121l-1.414 1.415l-2.121-2.122l-2.192 2.192a5.002 5.002 0 0 1-7.708 6.293a5 5 0 0 1 6.294-7.707m-.637 6.293A3 3 0 1 0 5.88 13.88a3 3 0 0 0 4.242 4.242"/>' |
| | | }, |
| | | 'layout-2-line': { |
| | | body: '<path fill="currentColor" d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1zM11 5H5v14h6zm8 8h-6v6h6zm0-8h-6v6h6z"/>' |
| | |
| | | 'layout-grid-line': { |
| | | body: '<path fill="currentColor" d="M21 3a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM11 13H4v6h7zm9 0h-7v6h7zm-9-8H4v6h7zm9 0h-7v6h7z"/>' |
| | | }, |
| | | 'loader-line': { |
| | | body: '<path fill="currentColor" d="M12 2a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1m0 15a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1m8.66-10a1 1 0 0 1-.366 1.366l-2.598 1.5a1 1 0 1 1-1-1.732l2.598-1.5A1 1 0 0 1 20.66 7M7.67 14.5a1 1 0 0 1-.367 1.366l-2.598 1.5a1 1 0 1 1-1-1.732l2.598-1.5a1 1 0 0 1 1.366.366M20.66 17a1 1 0 0 1-1.366.366l-2.598-1.5a1 1 0 0 1 1-1.732l2.598 1.5A1 1 0 0 1 20.66 17M7.67 9.5a1 1 0 0 1-1.367.366l-2.598-1.5a1 1 0 1 1 1-1.732l2.598 1.5A1 1 0 0 1 7.67 9.5"/>' |
| | | 'lightbulb-flash-line': { |
| | | body: '<path fill="currentColor" d="M9.973 18h4.054c.132-1.202.745-2.193 1.74-3.277c.113-.122.832-.867.917-.973a6 6 0 1 0-9.37-.002c.086.107.807.853.918.974c.996 1.084 1.609 2.076 1.741 3.278M14 20h-4v1h4zm-8.246-5a8 8 0 1 1 12.49.002C17.624 15.774 16 17 16 18.5V21a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-2.5C8 17 6.375 15.774 5.754 15M13 10.004h2.5l-4.5 6v-4H8.5L13 6z"/>' |
| | | }, |
| | | 'line-chart-line': { |
| | | body: '<path fill="currentColor" d="M5 3v16h16v2H3V3zm15.293 3.293l1.414 1.414L16 13.414l-3-2.999l-4.293 4.292l-1.414-1.414L13 7.586l3 2.999z"/>' |
| | | }, |
| | | 'lock-line': { |
| | | body: '<path fill="currentColor" d="M19 10h1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V11a1 1 0 0 1 1-1h1V9a7 7 0 0 1 14 0zM5 12v8h14v-8zm6 2h2v4h-2zm6-4V9A5 5 0 0 0 7 9v1z"/>' |
| | | }, |
| | | 'magic-line': { |
| | | body: '<path fill="currentColor" d="M15.199 9.944a2.6 2.6 0 0 1-.79-1.55l-.403-3.083l-2.731 1.486a2.6 2.6 0 0 1-1.719.272L6.5 6.5l.57 3.056a2.6 2.6 0 0 1-.273 1.72l-1.486 2.73l3.083.403a2.6 2.6 0 0 1 1.55.79l2.138 2.257l1.336-2.807a2.6 2.6 0 0 1 1.23-1.231l2.808-1.336zm.025 5.564l-2.213 4.65a.6.6 0 0 1-.977.155l-3.542-3.739a.6.6 0 0 0-.358-.182l-5.106-.668a.6.6 0 0 1-.45-.881l2.462-4.524a.6.6 0 0 0 .063-.396L4.16 4.86a.6.6 0 0 1 .7-.7l5.062.943a.6.6 0 0 0 .397-.063l4.523-2.46a.6.6 0 0 1 .882.448l.668 5.107a.6.6 0 0 0 .182.357l3.739 3.542a.6.6 0 0 1-.155.977l-4.65 2.213a.6.6 0 0 0-.284.284m.797 1.927l1.414-1.414l4.243 4.242l-1.415 1.415z"/>' |
| | | }, |
| | | 'mail-line': { |
| | | body: '<path fill="currentColor" d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m17 4.238l-7.928 7.1L4 7.216V19h16zM4.511 5l7.55 6.662L19.502 5z"/>' |
| | |
| | | body: '<path fill="currentColor" d="m12 20.9l4.95-4.95a7 7 0 1 0-9.9 0zm0 2.828l-6.364-6.364a9 9 0 1 1 12.728 0zM12 13a2 2 0 1 0 0-4a2 2 0 0 0 0 4m0 2a4 4 0 1 1 0-8a4 4 0 0 1 0 8"/>' |
| | | }, |
| | | 'menu-2-fill': { |
| | | body: '<path fill="currentColor" d="M3 4h18v2H3zm0 7h12v2H3zm0 7h18v2H3z"/>' |
| | | }, |
| | | 'menu-2-line': { |
| | | body: '<path fill="currentColor" d="M3 4h18v2H3zm0 7h12v2H3zm0 7h18v2H3z"/>' |
| | | }, |
| | | 'menu-line': { |
| | |
| | | 'message-3-line': { |
| | | body: '<path fill="currentColor" d="M2 8.994A5.99 5.99 0 0 1 8 3h8c3.313 0 6 2.695 6 5.994V21H8c-3.313 0-6-2.695-6-5.994zM20 19V8.994A4.004 4.004 0 0 0 16 5H8a3.99 3.99 0 0 0-4 3.994v6.012A4.004 4.004 0 0 0 8 19zm-6-8h2v2h-2zm-6 0h2v2H8z"/>' |
| | | }, |
| | | 'money-cny-box-line': { |
| | | body: '<path fill="currentColor" d="M3.005 3.003h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-18a1 1 0 0 1-1-1v-16a1 1 0 0 1 1-1m1 2v14h16v-14zm9 8h3v2h-3v2h-2v-2h-3v-2h3v-1h-3v-2h2.586L8.469 7.88l1.415-1.414l2.12 2.122l2.122-2.122L15.54 7.88l-2.12 2.122h2.585v2h-3z"/>' |
| | | }, |
| | | 'money-cny-circle-line': { |
| | | body: '<path fill="currentColor" d="M12.005 22.003c-5.523 0-10-4.477-10-10s4.477-10 10-10s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m1-7h3v2h-3v2h-2v-2h-3v-2h3v-1h-3v-2h2.586L8.469 7.88l1.415-1.414l2.12 2.122l2.122-2.122L15.54 7.88l-2.12 2.122h2.585v2h-3z"/>' |
| | | }, |
| | | 'money-dollar-circle-line': { |
| | | body: '<path fill="currentColor" d="M12.005 22.003c-5.523 0-10-4.477-10-10s4.477-10 10-10s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m-3.5-6h5.5a.5.5 0 1 0 0-1h-4a2.5 2.5 0 1 1 0-5h1v-2h2v2h2.5v2h-5.5a.5.5 0 0 0 0 1h4a2.5 2.5 0 0 1 0 5h-1v2h-2v-2h-2.5z"/>' |
| | | 'moon-line': { |
| | | body: '<path fill="currentColor" d="M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2h.1A6.98 6.98 0 0 0 10 7m-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938A8 8 0 0 0 4 12"/>' |
| | | }, |
| | | 'more-2-fill': { |
| | | body: '<path fill="currentColor" d="M12 3c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2m0 14c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2m0-7c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2"/>' |
| | | }, |
| | | 'mouse-line': { |
| | | body: '<path fill="currentColor" d="M11.141 4c-1.582 0-2.387.169-3.128.565a3.45 3.45 0 0 0-1.448 1.448C6.169 6.753 6 7.559 6 9.14v5.718c0 1.582.169 2.387.565 3.128q.504.944 1.448 1.448c.74.396 1.546.565 3.128.565h1.718c1.582 0 2.387-.169 3.128-.565a3.45 3.45 0 0 0 1.448-1.448c.396-.74.565-1.546.565-3.128V9.14c0-1.582-.169-2.387-.565-3.128a3.45 3.45 0 0 0-1.448-1.448C15.247 4.169 14.441 4 12.86 4zm0-2h1.718c2.014 0 3.094.278 4.071.801A5.45 5.45 0 0 1 19.2 5.07c.522.978.801 2.058.801 4.072v5.718c0 2.014-.279 3.094-.801 4.071a5.45 5.45 0 0 1-2.27 2.269c-.977.522-2.057.801-4.071.801H11.14c-2.014 0-3.094-.279-4.072-.801A5.45 5.45 0 0 1 4.8 18.931c-.522-.977-.8-2.057-.8-4.071V9.14c0-2.014.278-3.094.801-4.072A5.45 5.45 0 0 1 7.07 2.801C8.047 2.278 9.127 2 11.141 2M11 6h2v5h-2z"/>' |
| | | }, |
| | | 'notification-2-line': { |
| | | body: '<path fill="currentColor" d="M22 20H2v-2h1v-6.969C3 6.043 7.03 2 12 2s9 4.043 9 9.031V18h1zM5 18h14v-6.969C19 7.148 15.866 4 12 4s-7 3.148-7 7.031zm4.5 3h5a2.5 2.5 0 0 1-5 0"/>' |
| | |
| | | 'pencil-line': { |
| | | body: '<path fill="currentColor" d="m15.728 9.576l-1.414-1.414L5 17.476v1.414h1.414zm1.414-1.414l1.414-1.414l-1.414-1.414l-1.414 1.414zm-9.9 12.728H3v-4.243L16.435 3.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414z"/>' |
| | | }, |
| | | 'phone-line': { |
| | | body: '<path fill="currentColor" d="M9.366 10.682a10.56 10.56 0 0 0 3.952 3.952l.884-1.238a1 1 0 0 1 1.294-.296a11.4 11.4 0 0 0 4.583 1.364a1 1 0 0 1 .921.997v4.462a1 1 0 0 1-.898.995Q19.307 21 18.5 21C9.94 21 3 14.06 3 5.5q0-.807.082-1.602A1 1 0 0 1 4.077 3h4.462a1 1 0 0 1 .997.921A11.4 11.4 0 0 0 10.9 8.504a1 1 0 0 1-.296 1.294zm-2.522-.657l1.9-1.357A13.4 13.4 0 0 1 7.647 5H5.01Q5 5.25 5 5.5C5 12.956 11.044 19 18.5 19q.25 0 .5-.01v-2.637a13.4 13.4 0 0 1-3.668-1.097l-1.357 1.9a12.5 12.5 0 0 1-1.588-.75l-.058-.033a12.56 12.56 0 0 1-4.702-4.702l-.033-.058a12.4 12.4 0 0 1-.75-1.588"/>' |
| | | }, |
| | | 'pie-chart-line': { |
| | | body: '<path fill="currentColor" d="M9 2.458v2.124A8.003 8.003 0 0 0 12 20a8 8 0 0 0 7.419-5h2.123c-1.274 4.057-5.064 7-9.542 7c-5.523 0-10-4.477-10-10c0-4.478 2.943-8.268 7-9.542M12 2c5.523 0 10 4.477 10 10q0 .507-.05 1H11V2.05Q11.493 2 12 2m1 2.062V11h6.938A8.004 8.004 0 0 0 13 4.062"/>' |
| | | }, |
| | | 'planet-line': { |
| | | body: '<path fill="currentColor" d="M3.918 8.037A9 9 0 0 0 15.966 20.08c.873.373 1.719.618 2.49.681c.902.074 1.844-.095 2.526-.777c.752-.752.88-1.816.746-2.812c-.123-.91-.48-1.92-1.002-2.961A9 9 0 0 0 9.791 3.274c-1.044-.524-2.055-.882-2.965-1.006c-.997-.135-2.062-.007-2.815.746c-.682.683-.851 1.626-.777 2.528c.064.773.31 1.62.684 2.495m1.404-2.071a4 4 0 0 1-.095-.587c-.048-.586.09-.842.198-.95c.12-.12.423-.275 1.132-.179q.298.04.643.136a9 9 0 0 0-1.878 1.58m14.29 10.837a5 5 0 0 1 .134.637c.096.709-.06 1.012-.178 1.13c-.109.109-.364.247-.95.199a4 4 0 0 1-.581-.094a9 9 0 0 0 1.575-1.872m-3.73 1.023c-1.677-.878-3.625-2.323-5.507-4.205c-1.88-1.88-3.324-3.825-4.203-5.5A7.02 7.02 0 0 1 9.97 5.298a7 7 0 0 1 5.912 12.528m-2.277.99a7 7 0 0 1-8.42-8.419c.964 1.516 2.25 3.112 3.776 4.638c1.528 1.528 3.126 2.815 4.644 3.78"/>' |
| | | 'plug-2-line': { |
| | | body: '<path fill="currentColor" d="M13 18v2h6v2h-6a2 2 0 0 1-2-2v-2H8a4 4 0 0 1-4-4V7a1 1 0 0 1 1-1h2V2h2v4h6V2h2v4h2a1 1 0 0 1 1 1v7a4 4 0 0 1-4 4zm-5-2h8a2 2 0 0 0 2-2v-3H6v3a2 2 0 0 0 2 2m10-8H6v1h12zm-6 6.5a1 1 0 1 1 0-2a1 1 0 0 1 0 2M11 2h2v3h-2z"/>' |
| | | }, |
| | | 'price-tag-line': { |
| | | body: '<path fill="currentColor" d="m3.005 7l8.445-5.63a1 1 0 0 1 1.11 0L21.005 7v14a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1zm2 1.07V20h14V8.07l-7-4.667zm7 2.93a2 2 0 1 1 0-4a2 2 0 0 1 0 4"/>' |
| | | }, |
| | | 'profile-line': { |
| | | body: '<path fill="currentColor" d="M21.008 3c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3zM20 5H4v14h16zm-2 10v2H6v-2zm-6-8v6H6V7zm6 4v2h-4v-2zm-8-2H8v2h2zm8-2v2h-4V7z"/>' |
| | | }, |
| | | 'progress-2-line': { |
| | | body: '<path fill="currentColor" d="M2 12c0 5.523 4.477 10 10 10s10-4.477 10-10S17.523 2 12 2S2 6.477 2 12m18 0a8 8 0 1 1-16 0a8 8 0 0 1 16 0m-8 0V6a6 6 0 0 1 6 6z"/>' |
| | |
| | | 'pushpin-2-line': { |
| | | body: '<path fill="currentColor" d="M18 3v2h-1v6l2 3v2h-6v7h-2v-7H5v-2l2-3V5H6V3zM9 5v6.606L7.404 14h9.192L15 11.606V5z"/>' |
| | | }, |
| | | 'qr-code-line': { |
| | | body: '<path fill="currentColor" d="M16 17v-1h-3v-3h3v2h2v2h-1v2h-2v2h-2v-3h2v-1zm5 4h-4v-2h2v-2h2zM3 3h8v8H3zm2 2v4h4V5zm8-2h8v8h-8zm2 2v4h4V5zM3 13h8v8H3zm2 2v4h4v-4zm13-2h3v2h-3zM6 6h2v2H6zm0 10h2v2H6zM16 6h2v2h-2z"/>' |
| | | }, |
| | | 'rectangle-line': { |
| | | body: '<path fill="currentColor" d="M3 4h18a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1m1 2v12h16V6z"/>' |
| | | }, |
| | | 'refresh-line': { |
| | | body: '<path fill="currentColor" d="M5.463 4.433A9.96 9.96 0 0 1 12 2c5.523 0 10 4.477 10 10c0 2.136-.67 4.116-1.81 5.74L17 12h3A8 8 0 0 0 6.46 6.228zm13.074 15.134A9.96 9.96 0 0 1 12 22C6.477 22 2 17.523 2 12c0-2.136.67-4.116 1.81-5.74L7 12H4a8 8 0 0 0 13.54 5.772z"/>' |
| | | }, |
| | | 'screenshot-line': { |
| | | body: '<path fill="currentColor" d="m11.993 14.407l-1.552 1.552a4 4 0 1 1-1.418-1.41l1.555-1.556l-4.185-4.185l1.415-1.415l4.185 4.185l4.189-4.189l1.414 1.414l-4.19 4.19l1.562 1.56a4 4 0 1 1-1.414 1.414zM7 20a2 2 0 1 0 0-4a2 2 0 0 0 0 4m10 0a2 2 0 1 0 0-4a2 2 0 0 0 0 4m2-7V5H5v8H3V4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v9z"/>' |
| | | 'robot-2-line': { |
| | | body: '<path fill="currentColor" d="M13.5 2c0 .444-.193.843-.5 1.118V5h5a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3h5V3.118A1.5 1.5 0 1 1 13.5 2M6 7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1zm-4 3H0v6h2zm20 0h2v6h-2zM9 14.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m6 0a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3"/>' |
| | | }, |
| | | 'route-line': { |
| | | body: '<path fill="currentColor" d="M4 15V8.5a4.5 4.5 0 0 1 9 0v7a2.5 2.5 0 0 0 5 0V8.83a3.001 3.001 0 1 1 2 0v6.67a4.5 4.5 0 1 1-9 0v-7a2.5 2.5 0 0 0-5 0V15h3l-4 5l-4-5zm15-8a1 1 0 1 0 0-2a1 1 0 0 0 0 2"/>' |
| | | }, |
| | | 'search-line': { |
| | | body: '<path fill="currentColor" d="m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z"/>' |
| | | }, |
| | | 'server-line': { |
| | | body: '<path fill="currentColor" d="M5 11h14V5H5zm16-7v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1m-2 9H5v6h14zM7 15h3v2H7zm0-8h3v2H7z"/>' |
| | | }, |
| | | 'settings-line': { |
| | | body: '<path fill="currentColor" d="m12 1l9.5 5.5v11L12 23l-9.5-5.5v-11zm0 2.311L4.5 7.653v8.694l7.5 4.342l7.5-4.342V7.653zM12 16a4 4 0 1 1 0-8a4 4 0 0 1 0 8m0-2a2 2 0 1 0 0-4a2 2 0 0 0 0 4"/>' |
| | | }, |
| | | 'shake-hands-line': { |
| | | body: '<path fill="currentColor" d="M11.861 2.39a3 3 0 0 1 3.275-.034L19.29 5H21a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-1.52a2.65 2.65 0 0 1-1.285 2.449l-5.093 3.056a2 2 0 0 1-2.07-.008a2 2 0 0 1-2.561.073l-5.14-4.039a2 2 0 0 1-.565-2.446A2 2 0 0 1 2 13.51V6a1 1 0 0 1 1-1h4.947zM4.173 13.646l.692-.605a3 3 0 0 1 4.195.24l2.702 2.972a3 3 0 0 1 .396 3.487l5.009-3.005a.66.66 0 0 0 .278-.79l-4.427-6.198a1 1 0 0 0-1.101-.377l-2.486.745a3 3 0 0 1-2.983-.752l-.293-.292A2 2 0 0 1 5.68 7H4v6.51zm9.89-9.602a1 1 0 0 0-1.093.012l-5.4 3.6l.292.293a1 1 0 0 0 .995.25l2.485-.745a3 3 0 0 1 3.303 1.13L18.515 14H20V7h-.709a2 2 0 0 1-1.074-.313zM6.181 14.545l-1.616 1.414l5.14 4.039l.705-1.232a1 1 0 0 0-.129-1.169L7.58 14.625a1 1 0 0 0-1.398-.08"/>' |
| | | }, |
| | | 'share-forward-line': { |
| | | body: '<path fill="currentColor" d="M13 14h-2a9 9 0 0 0-7.968 4.81A10 10 0 0 1 3 18C3 12.477 7.477 8 13 8V2.5L23.5 11L13 19.5zm-2-2h4v3.308L20.321 11L15 6.692V10h-2a7.98 7.98 0 0 0-6.057 2.774A11 11 0 0 1 11 12"/>' |
| | | }, |
| | | 'shield-check-line': { |
| | | body: '<path fill="currentColor" d="m12 1l8.217 1.826a1 1 0 0 1 .783.976v9.987a6 6 0 0 1-2.672 4.992L12 23l-6.328-4.219A6 6 0 0 1 3 13.79V3.802a1 1 0 0 1 .783-.976zm0 2.049L5 4.604v9.185a4 4 0 0 0 1.781 3.328L12 20.597l5.219-3.48A4 4 0 0 0 19 13.79V4.604zm4.452 5.173l1.415 1.414L11.503 16L7.26 11.757l1.414-1.414l2.828 2.828z"/>' |
| | | }, |
| | | 'shopping-bag-3-line': { |
| | | body: '<path fill="currentColor" d="M6.505 2h11a1 1 0 0 1 .8.4l2.7 3.6v15a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1V6l2.7-3.6a1 1 0 0 1 .8-.4m12.5 6h-14v12h14zm-.5-2l-1.5-2h-10l-1.5 2zm-9.5 4v2a3 3 0 1 0 6 0v-2h2v2a5 5 0 0 1-10 0v-2z"/>' |
| | | 'smartphone-line': { |
| | | body: '<path fill="currentColor" d="M7 4v16h10V4zM6 2h12a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1m6 15a1 1 0 1 1 0 2a1 1 0 0 1 0-2"/>' |
| | | }, |
| | | 'shopping-bag-4-line': { |
| | | body: '<path fill="currentColor" d="M9 6h6a3 3 0 1 0-6 0M7 6a5 5 0 0 1 10 0h3a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zM5 8v12h14V8zm4 2a3 3 0 1 0 6 0h2a5 5 0 0 1-10 0z"/>' |
| | | 'store-2-line': { |
| | | body: '<path fill="currentColor" d="M21 13.242V20h1v2H2v-2h1v-6.758A4.5 4.5 0 0 1 1 9.5c0-.827.224-1.624.633-2.303L4.345 2.5a1 1 0 0 1 .866-.5H18.79a1 1 0 0 1 .866.5l2.703 4.682c.418.694.642 1.49.642 2.318c0 1.56-.794 2.935-2 3.742m-2 .73a4.5 4.5 0 0 1-3.75-1.36A4.5 4.5 0 0 1 12 14.001a4.5 4.5 0 0 1-3.25-1.387A4.5 4.5 0 0 1 5 13.973V20h14zM5.789 4L3.356 8.213a2.5 2.5 0 1 0 4.466 2.216c.335-.837 1.52-.837 1.856 0a2.5 2.5 0 0 0 4.644 0c.335-.837 1.52-.837 1.856 0a2.5 2.5 0 1 0 4.457-2.232L18.21 4z"/>' |
| | | }, |
| | | 'shopping-bag-line': { |
| | | body: '<path fill="currentColor" d="M7.005 8V6a5 5 0 0 1 10 0v2h3a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-16a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1zm0 2h-2v10h14V10h-2v2h-2v-2h-6v2h-2zm2-2h6V6a3 3 0 0 0-6 0z"/>' |
| | | }, |
| | | 'sparkling-line': { |
| | | body: '<path fill="currentColor" d="M14 4.438A2.437 2.437 0 0 0 16.438 2h1.125A2.437 2.437 0 0 0 20 4.438v1.125A2.437 2.437 0 0 0 17.563 8h-1.125A2.437 2.437 0 0 0 14 5.563zM1 11a6 6 0 0 0 6-6h2a6 6 0 0 0 6 6v2a6 6 0 0 0-6 6H7a6 6 0 0 0-6-6zm3.876 1A8.04 8.04 0 0 1 8 15.124A8.04 8.04 0 0 1 11.124 12A8.04 8.04 0 0 1 8 8.876A8.04 8.04 0 0 1 4.876 12m12.374 2A3.25 3.25 0 0 1 14 17.25v1.5A3.25 3.25 0 0 1 17.25 22h1.5A3.25 3.25 0 0 1 22 18.75v-1.5A3.25 3.25 0 0 1 18.75 14z"/>' |
| | | }, |
| | | 'star-fill': { |
| | | body: '<path fill="currentColor" d="m12 18.26l-7.053 3.948l1.575-7.928L.588 8.792l8.027-.952L12 .5l3.385 7.34l8.027.952l-5.934 5.488l1.575 7.928z"/>' |
| | | }, |
| | | 'subway-line': { |
| | | body: '<path fill="currentColor" d="m17.2 20l1.8 1.5v.5H5v-.5L6.8 20H5a2 2 0 0 1-2-2V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4v11a2 2 0 0 1-2 2zM13 5v6h6V7a2 2 0 0 0-2-2zm-2 0H7a2 2 0 0 0-2 2v4h6zm8 8H5v5h14zM7.5 17a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3m9 0a1.5 1.5 0 1 1 0-3a1.5 1.5 0 0 1 0 3"/>' |
| | | }, |
| | | 't-box-line': { |
| | | body: '<path fill="currentColor" d="M5 5v14h14V5zM4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m9 7v7h-2v-7H7V8h10v2z"/>' |
| | | }, |
| | | 'table-3': { |
| | | body: '<path fill="currentColor" d="M3 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm8 2v3H4V5zm-7 9v-4h7v4zm0 2h7v3H4zm9 0h7v3h-7zm7-2h-7v-4h7zm0-9v3h-7V5z"/>' |
| | | 'sun-fill': { |
| | | body: '<path fill="currentColor" d="M12 18a6 6 0 1 1 0-12a6 6 0 0 1 0 12M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636L5.636 7.05zM16.95 18.364l1.414-1.414l2.121 2.121l-1.414 1.414zm2.121-14.85l1.414 1.415l-2.121 2.121l-1.414-1.414zM5.636 16.95l1.414 1.414l-2.121 2.121l-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z"/>' |
| | | }, |
| | | 'table-line': { |
| | | body: '<path fill="currentColor" d="M4 8h16V5H4zm10 11v-9h-4v9zm2 0h4v-9h-4zm-8 0v-9H4v9zM3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1"/>' |
| | | }, |
| | | 'table-view': { |
| | | body: '<path fill="currentColor" d="M3 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm5 2v3H4V5zm-4 9v-4h4v4zm0 2h4v3H4zm6 0h10v3H10zm10-2H10v-4h10zm0-9v3H10V5z"/>' |
| | | }, |
| | | 'telegram-2-line': { |
| | | body: '<path fill="currentColor" d="M17.094 7.146c.593-.215.888-.292 1.05-.32q.002.08-.002.122c-.232 2.444-1.251 8.457-1.775 11.255c-.122.655-.216.967-.85.595c-.416-.245-.792-.553-1.196-.817c-1.325-.869-3.221-2.162-3.065-2.084c-1.304-.86-.758-1.386-.03-2.088c.117-.113.24-.231.36-.356c.054-.056.317-.3.687-.645c1.188-1.104 3.484-3.239 3.542-3.486c.01-.04.018-.192-.071-.271c-.09-.08-.223-.053-.318-.031q-.203.046-6.474 4.279q-.918.63-1.664.614l.005.003c-.655-.231-1.308-.43-1.964-.63a66 66 0 0 1-1.3-.405l-.308-.098c4.527-1.972 7.542-3.27 9.053-3.899c2.194-.913 3.496-1.438 4.32-1.738m2.423-1.928a1.8 1.8 0 0 0-.726-.346c-.2-.048-.39-.063-.533-.06c-.477.008-.988.143-1.846.454c-.875.318-2.219.862-4.406 1.771Q9.691 8 2.804 11.001c-.404.161-.773.344-1.065.56c-.27.201-.647.56-.716 1.11c-.052.416.069.8.315 1.103c.214.263.488.423.697.524c.31.15.728.281 1.095.396c.573.18 1.144.363 1.719.539c1.778.544 3.242.992 4.852 2.054c1.181.778 2.34 1.59 3.523 2.366c.432.283.835.608 1.28.87c.488.285 1.106.546 1.86.477c1.138-.105 1.73-1.152 1.97-2.43c.521-2.79 1.557-8.886 1.8-11.432a3.8 3.8 0 0 0-.037-.885a1.66 1.66 0 0 0-.58-1.035"/>' |
| | | }, |
| | | 'thumb-up-line': { |
| | | body: '<path fill="currentColor" d="M14.6 8H21a2 2 0 0 1 2 2v2.105c0 .26-.051.52-.15.761l-3.095 7.515a1 1 0 0 1-.925.62H2a1 1 0 0 1-1-1V10a1 1 0 0 1 1-1h3.482a1 1 0 0 0 .817-.424L11.752.851a.5.5 0 0 1 .632-.159l1.814.908a2.5 2.5 0 0 1 1.305 2.852zM7 10.588V19h11.16L21 12.105V10h-6.4a2 2 0 0 1-1.938-2.493l.903-3.548a.5.5 0 0 0-.261-.57l-.661-.331l-4.71 6.672c-.25.354-.57.645-.933.858M5 11H3v8h2z"/>' |
| | | }, |
| | | 'time-line': { |
| | | body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m1-8h4v2h-6V7h2z"/>' |
| | | 'tools-line': { |
| | | body: '<path fill="currentColor" d="M5.33 3.272a3.5 3.5 0 0 1 4.254 4.962l10.709 10.71l-1.414 1.414l-10.71-10.71a3.502 3.502 0 0 1-4.962-4.255L5.444 7.63a1.5 1.5 0 0 0 2.121-2.121zm10.367 1.883l3.182-1.768l1.414 1.415l-1.768 3.182l-1.768.353l-2.12 2.121l-1.415-1.414l2.121-2.121zm-6.718 8.132l1.415 1.414l-5.304 5.303a1 1 0 0 1-1.492-1.327l.078-.087z"/>' |
| | | }, |
| | | 'translate-2': { |
| | | body: '<path fill="currentColor" d="m18.5 10l4.4 11h-2.155l-1.201-3h-4.09l-1.199 3h-2.154L16.5 10zM10 2v2h6v2h-1.968a18.2 18.2 0 0 1-3.62 6.301a15 15 0 0 0 2.335 1.707l-.75 1.878A17 17 0 0 1 9 13.725a16.7 16.7 0 0 1-6.201 3.548l-.536-1.929a14.7 14.7 0 0 0 5.327-3.042A18 18 0 0 1 4.767 8h2.24A16 16 0 0 0 9 10.877a16.2 16.2 0 0 0 2.91-4.876L2 6V4h6V2zm7.5 10.885L16.253 16h2.492z"/>' |
| | | }, |
| | | 'twitch-line': { |
| | | body: '<path fill="currentColor" d="M4.301 3h16.7v11.7l-4.7 4.7h-3.9l-2.5 2.4h-2.9v-2.4h-4V6.2zm.7 14.4h4v2.4h.095l2.5-2.4h3.877L19 13.872V5H5zm10-9.4h2v4.7h-2zm0 0h2v4.7h-2zm-5 0h2v4.7h-2z"/>' |
| | | 'unpin-line': { |
| | | body: '<path fill="currentColor" d="m20.97 17.172l-1.414 1.414l-3.535-3.535l-.073.074l-.707 3.536l-1.415 1.414l-4.242-4.243l-4.95 4.95l-1.414-1.414l4.95-4.95l-4.243-4.243L5.34 8.761l3.536-.707l.073-.074l-3.536-3.536L6.828 3.03zM10.365 9.394l-.502.502l-2.822.565l6.5 6.5l.564-2.822l.502-.502zm8.411.074l-1.34 1.34l1.414 1.415l1.34-1.34l.707.707l1.415-1.415l-8.486-8.485l-1.414 1.414l.707.707l-1.34 1.34l1.414 1.415l1.34-1.34z"/>' |
| | | }, |
| | | 'user-3-line': { |
| | | body: '<path fill="currentColor" d="M20 22h-2v-2a3 3 0 0 0-3-3H9a3 3 0 0 0-3 3v2H4v-2a5 5 0 0 1 5-5h6a5 5 0 0 1 5 5zm-8-9a6 6 0 1 1 0-12a6 6 0 0 1 0 12m0-2a4 4 0 1 0 0-8a4 4 0 0 0 0 8"/>' |
| | |
| | | 'user-settings-line': { |
| | | body: '<path fill="currentColor" d="M12 14v2a6 6 0 0 0-6 6H4a8 8 0 0 1 8-8m0-1c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4m2.595 7.811a3.5 3.5 0 0 1 0-1.622l-.992-.573l1-1.732l.992.573A3.5 3.5 0 0 1 17 14.645V13.5h2v1.145c.532.158 1.012.44 1.405.812l.992-.573l1 1.732l-.991.573a3.5 3.5 0 0 1 0 1.622l.991.573l-1 1.732l-.992-.573a3.5 3.5 0 0 1-1.405.812V22.5h-2v-1.145a3.5 3.5 0 0 1-1.405-.812l-.992.573l-1-1.732zM18 19.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3"/>' |
| | | }, |
| | | 'video-on-line': { |
| | | body: '<path fill="currentColor" d="m17 9.2l5.213-3.65a.5.5 0 0 1 .787.41v12.08a.5.5 0 0 1-.787.41L17 14.8V19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1zm0 3.159l4 2.8V8.84l-4 2.8zM3 6v12h12V6z"/>' |
| | | }, |
| | | 'vidicon-line': { |
| | | body: '<path fill="currentColor" d="m17 9.2l5.213-3.65a.5.5 0 0 1 .787.41v12.08a.5.5 0 0 1-.787.41L17 14.8V19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1zm0 3.159l4 2.8V8.84l-4 2.8zM3 6v12h12V6zm2 2h2v2H5z"/>' |
| | | }, |
| | | 'volume-down-line': { |
| | | body: '<path fill="currentColor" d="M13 7.22L9.603 10H6v4h3.603L13 16.78zM8.889 16H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h3.889l5.294-4.332a.5.5 0 0 1 .817.387v15.89a.5.5 0 0 1-.817.387zm9.974.591l-1.422-1.422A4 4 0 0 0 19 12c0-1.43-.75-2.685-1.88-3.392l1.439-1.439A5.99 5.99 0 0 1 21 12c0 1.842-.83 3.49-2.137 4.591"/>' |
| | | }, |
| | | 'wallet-line': { |
| | | body: '<path fill="currentColor" d="M18.005 7h3a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h15zm-14 2v10h16V9zm0-4v2h12V5zm11 8h3v2h-3z"/>' |
| | | }, |
| | | 'water-flash-line': { |
| | | body: '<path fill="currentColor" d="m12.005 3.103l-4.95 4.95a7 7 0 1 0 9.9 0zm0-2.828l6.364 6.364A9 9 0 1 1 5.64 19.367a9 9 0 0 1 0-12.728zm1 10.728h2.5l-4.5 6.5v-4.5h-2.5l4.5-6.5z"/>' |
| | | }, |
| | | 'windows-line': { |
| | | body: '<path fill="currentColor" d="M21.001 2.5v19l-18-2v-15zm-2 10.499l-7 .001v5.487l7 .779zm-14 4.71l5 .556V13l-5-.001zm14-6.71V4.735l-7 .777V11zm-9-5.265l-5 .556V11l5 .001z"/>' |
| | | } |
| | | }, |
| | | prefix: 'ri', |
| New file |
| | |
| | | import { RoutesAlias } from '../routesAlias.js' |
| | | import { resolveBackendMenuTitle } from '../../utils/backend-menu-title.js' |
| | | |
| | | const PHASE_1_COMPONENTS = { |
| | | console: '/dashboard/console', |
| | | user: '/system/user', |
| | | role: '/system/role', |
| | | menu: '/system/menu', |
| | | userLogin: '/system/user-login' |
| | | } |
| | | |
| | | const LEGACY_BACKEND_ICON_MAP = Object.freeze({ |
| | | SmartToy: 'ri:robot-2-line', |
| | | Psychology: 'ri:lightbulb-flash-line', |
| | | History: 'ri:history-line', |
| | | Cable: 'ri:plug-2-line', |
| | | StorageSharp: 'ri:server-line', |
| | | Warehouse: 'ri:store-2-line', |
| | | Inventory2Outlined: 'ri:archive-line', |
| | | DeckOutlined: 'ri:layout-grid-line', |
| | | FormatListNumberedOutlined: 'ri:file-list-3-line', |
| | | Beenhere: 'ri:checkbox-circle-line', |
| | | ManageHistoryOutlined: 'ri:history-line', |
| | | AssessmentOutlined: 'ri:bar-chart-box-line', |
| | | QueryStats: 'ri:line-chart-line', |
| | | ScreenSearchDesktop: 'ri:computer-line', |
| | | Dvr: 'ri:file-list-3-line', |
| | | Token: 'ri:key-2-line', |
| | | GroupAddOutlined: 'ri:user-add-line', |
| | | People: 'ri:group-line', |
| | | Style: 'ri:price-tag-line', |
| | | Groups: 'ri:group-line', |
| | | SettingsOutlined: 'ri:settings-line', |
| | | MenuOpen: 'ri:menu-unfold-3-line', |
| | | Straighten: 'ri:table-line', |
| | | MenuBook: 'ri:book-2-line', |
| | | ConfirmationNumber: 'ri:price-tag-line', |
| | | Engineering: 'ri:tools-line', |
| | | TripOrigin: 'ri:checkbox-blank-circle-line' |
| | | }) |
| | | |
| | | function adaptBackendMenuTree(menuTree) { |
| | | if (!Array.isArray(menuTree)) { |
| | | return [] |
| | | } |
| | | return menuTree |
| | | .map((item) => adaptMenuNode(item, { depth: 0, parentRoutePath: '' })) |
| | | .filter(Boolean) |
| | | } |
| | | |
| | | function adaptMenuNode(node, context) { |
| | | if (!node || typeof node !== 'object') { |
| | | return null |
| | | } |
| | | |
| | | if (node.type !== void 0 && node.type !== 0) { |
| | | return null |
| | | } |
| | | |
| | | const { depth, parentRoutePath } = context |
| | | const rawRoute = typeof node.route === 'string' ? node.route.trim() : '' |
| | | const fullRoutePath = buildFullRoutePath(rawRoute, parentRoutePath) |
| | | const children = Array.isArray(node.children) |
| | | ? node.children |
| | | .map((item) => |
| | | adaptMenuNode(item, { |
| | | depth: depth + 1, |
| | | parentRoutePath: fullRoutePath || parentRoutePath |
| | | }) |
| | | ) |
| | | .filter(Boolean) |
| | | : [] |
| | | const hasChildren = children.length > 0 |
| | | const component = resolveComponent(node.component, fullRoutePath, hasChildren) |
| | | const isFirstLevel = depth === 0 |
| | | |
| | | if (!hasChildren && !component) { |
| | | return null |
| | | } |
| | | |
| | | const path = resolvePath(node, { component, isFirstLevel, hasChildren, rawRoute }) |
| | | const meta = buildMeta(node) |
| | | const adapted = { |
| | | id: normalizeId(node.id), |
| | | name: buildRouteName(node, path), |
| | | path, |
| | | meta |
| | | } |
| | | |
| | | if (hasChildren) { |
| | | adapted.children = children |
| | | } |
| | | |
| | | if (component) { |
| | | adapted.component = component |
| | | } else if (isFirstLevel) { |
| | | adapted.component = RoutesAlias.Layout |
| | | } |
| | | |
| | | return adapted |
| | | } |
| | | |
| | | function resolveComponent(componentKey, fullRoutePath, hasChildren) { |
| | | const normalizedKey = typeof componentKey === 'string' ? componentKey.trim() : '' |
| | | |
| | | if (!normalizedKey && hasChildren) { |
| | | return '' |
| | | } |
| | | |
| | | if (!normalizedKey) { |
| | | return normalizeComponentPath(fullRoutePath) |
| | | } |
| | | |
| | | return PHASE_1_COMPONENTS[normalizedKey] || normalizeComponentPath(fullRoutePath) |
| | | } |
| | | |
| | | function resolvePath(node, { component, isFirstLevel, hasChildren, rawRoute }) { |
| | | if (rawRoute) { |
| | | if (!isFirstLevel && rawRoute.startsWith('/')) { |
| | | return normalizeComponentPath(rawRoute) |
| | | } |
| | | return sanitizePath(rawRoute) |
| | | } |
| | | if (component) { |
| | | const componentPath = component.replace(/^\//, '') |
| | | return isFirstLevel ? `/${componentPath}` : componentPath.split('/').pop() || '' |
| | | } |
| | | const rawPath = typeof node.path === 'string' ? node.path.trim() : '' |
| | | if (rawPath) { |
| | | return sanitizePath(rawPath) |
| | | } |
| | | return '' |
| | | } |
| | | |
| | | function buildMeta(node) { |
| | | const meta = { |
| | | title: normalizeTitle(node.name || node.meta?.title || '') |
| | | } |
| | | const metaSource = node.meta && typeof node.meta === 'object' ? node.meta : node |
| | | const supportedKeys = [ |
| | | 'icon', |
| | | 'keepAlive', |
| | | 'isHide', |
| | | 'isHideTab', |
| | | 'fixedTab', |
| | | 'link', |
| | | 'isIframe', |
| | | 'authList', |
| | | 'roles' |
| | | ] |
| | | |
| | | supportedKeys.forEach((key) => { |
| | | if (metaSource[key] !== void 0) { |
| | | meta[key] = key === 'icon' ? normalizeIcon(metaSource[key]) : metaSource[key] |
| | | } |
| | | }) |
| | | |
| | | return meta |
| | | } |
| | | |
| | | function normalizeTitle(title) { |
| | | if (typeof title !== 'string') { |
| | | return '' |
| | | } |
| | | return resolveBackendMenuTitle(title) |
| | | } |
| | | |
| | | function normalizeIcon(icon) { |
| | | if (typeof icon !== 'string') { |
| | | return icon |
| | | } |
| | | const normalizedIcon = icon.trim() |
| | | if (!normalizedIcon) { |
| | | return '' |
| | | } |
| | | if (normalizedIcon.includes(':')) { |
| | | return normalizedIcon |
| | | } |
| | | return LEGACY_BACKEND_ICON_MAP[normalizedIcon] || normalizedIcon |
| | | } |
| | | |
| | | function buildRouteName(node, path) { |
| | | if (typeof node.name === 'string' && node.name.trim()) { |
| | | return node.name.trim() |
| | | } |
| | | if (typeof node.component === 'string' && node.component.trim()) { |
| | | return node.component.trim() |
| | | } |
| | | if (path) { |
| | | return path |
| | | .split('/') |
| | | .filter(Boolean) |
| | | .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) |
| | | .join('') |
| | | } |
| | | return `Menu${normalizeId(node.id)}` |
| | | } |
| | | |
| | | function normalizeId(id) { |
| | | if (id === null || id === void 0 || id === '') { |
| | | return '' |
| | | } |
| | | return String(id) |
| | | } |
| | | |
| | | function sanitizePath(path) { |
| | | return path.replace(/^\/+/, '').replace(/\/+$/, '') |
| | | } |
| | | |
| | | function normalizeComponentPath(fullRoutePath) { |
| | | if (!fullRoutePath) { |
| | | return '' |
| | | } |
| | | return `/${sanitizePath(fullRoutePath)}` |
| | | } |
| | | |
| | | function buildFullRoutePath(rawRoute, parentRoutePath) { |
| | | if (!rawRoute) { |
| | | return normalizeComponentPath(parentRoutePath) |
| | | } |
| | | |
| | | if (rawRoute.startsWith('/')) { |
| | | return normalizeComponentPath(rawRoute) |
| | | } |
| | | |
| | | const normalizedParent = sanitizePath(parentRoutePath || '') |
| | | const normalizedRoute = sanitizePath(rawRoute) |
| | | |
| | | if (!normalizedRoute) { |
| | | return normalizeComponentPath(normalizedParent) |
| | | } |
| | | |
| | | if (!normalizedParent) { |
| | | return `/${normalizedRoute}` |
| | | } |
| | | |
| | | return `/${normalizedParent}/${normalizedRoute}` |
| | | } |
| | | |
| | | export { LEGACY_BACKEND_ICON_MAP, PHASE_1_COMPONENTS, adaptBackendMenuTree, normalizeIcon } |
| | |
| | | */ |
| | | isValidAbsolutePath(path) { |
| | | return ( |
| | | path.startsWith('/') || |
| | | path.startsWith('http://') || |
| | | path.startsWith('https://') || |
| | | path.startsWith('/outside/iframe/') |
| | |
| | | import { ref, computed } from 'vue' |
| | | import { router } from '@/router' |
| | | import { useCommon } from '@/hooks/core/useCommon' |
| | | import { normalizeIcon } from '@/router/adapters/backendMenuAdapter.js' |
| | | |
| | | const normalizeTabState = (tab) => { |
| | | if (!tab || typeof tab !== 'object') { |
| | | return tab |
| | | } |
| | | |
| | | return { |
| | | ...tab, |
| | | icon: normalizeIcon(tab.icon) |
| | | } |
| | | } |
| | | |
| | | const useWorktabStore = defineStore( |
| | | 'worktabStore', |
| | | () => { |
| | |
| | | } |
| | | if (existingIndex === -1) { |
| | | const insertIndex = tab.fixedTab ? findFixedTabInsertIndex() : opened.value.length |
| | | const newTab = { ...tab } |
| | | const newTab = normalizeTabState(tab) |
| | | if (tab.fixedTab) { |
| | | opened.value.splice(insertIndex, 0, newTab) |
| | | } else { |
| | |
| | | current.value = newTab |
| | | } else { |
| | | const existingTab = opened.value[existingIndex] |
| | | opened.value[existingIndex] = { |
| | | opened.value[existingIndex] = normalizeTabState({ |
| | | ...existingTab, |
| | | path: tab.path, |
| | | params: tab.params, |
| | |
| | | keepAlive: tab.keepAlive ?? existingTab.keepAlive, |
| | | name: tab.name || existingTab.name, |
| | | icon: tab.icon || existingTab.icon |
| | | } |
| | | }) |
| | | current.value = opened.value[existingIndex] |
| | | } |
| | | } |
| | |
| | | return false |
| | | } |
| | | } |
| | | const validTabs = opened.value.filter((tab) => isTabRouteValid(tab)) |
| | | const validTabs = opened.value.filter((tab) => isTabRouteValid(tab)).map(normalizeTabState) |
| | | if (validTabs.length !== opened.value.length) { |
| | | console.warn('发现无效的标签页路由,已自动清理') |
| | | } |
| | | if ( |
| | | validTabs.length !== opened.value.length || |
| | | validTabs.some((tab, index) => tab.icon !== opened.value[index]?.icon) |
| | | ) { |
| | | opened.value = validTabs |
| | | } |
| | | const isCurrentValid = current.value && isTabRouteValid(current.value) |
| | | if (!isCurrentValid && validTabs.length > 0) { |
| | | console.warn('当前激活标签无效,已自动切换') |
| | | current.value = validTabs[0] |
| | | } else if (isCurrentValid) { |
| | | current.value = normalizeTabState(current.value) |
| | | } else if (!isCurrentValid) { |
| | | current.value = {} |
| | | } |
| New file |
| | |
| | | const LEGACY_BACKEND_MENU_TITLES = { |
| | | 'menu.abnormal': '异常管理', |
| | | 'menu.aiCallLog': 'AI 观测', |
| | | 'menu.aiMcpMount': 'MCP 挂载', |
| | | 'menu.aiParam': 'AI 参数', |
| | | 'menu.aiPrompt': 'Prompt 管理', |
| | | 'menu.asnOrder': '入库通知单', |
| | | 'menu.asnOrderItem': '收货明细', |
| | | 'menu.asnOrderItemLog': '收货历史明细', |
| | | 'menu.asnOrderLog': '历史通知单', |
| | | 'menu.basContainer': '容器规则', |
| | | 'menu.basStation': '站点管理', |
| | | 'menu.basStationArea': '站点区域', |
| | | 'menu.basicInfo': '基础信息', |
| | | 'menu.check': '盘点管理', |
| | | 'menu.checkDiff': '盘点差异单', |
| | | 'menu.checkItem': '盘点单明细', |
| | | 'menu.checkOrder': '盘点单', |
| | | 'menu.checkOutBound': '盘点出库', |
| | | 'menu.companys': '往来企业', |
| | | 'menu.config': '配置参数', |
| | | 'menu.container': '容器管理(废)', |
| | | 'menu.contract': '合同信息(废)', |
| | | 'menu.customer': '客户表', |
| | | 'menu.dashboard': '控制台', |
| | | 'menu.delivery': 'DO单', |
| | | 'menu.deliveryItem': 'DO单明细', |
| | | 'menu.department': '部门管理', |
| | | 'menu.deviceBind': '设备绑定', |
| | | 'menu.deviceSite': '路径管理', |
| | | 'menu.dictData': '字典数据集', |
| | | 'menu.dictType': '数据字典', |
| | | 'menu.fields': '扩展字段', |
| | | 'menu.fieldsItem': '扩展字段明细', |
| | | 'menu.flowInstance': '流程实例', |
| | | 'menu.flowStepInstance': '流程步骤实例', |
| | | 'menu.flowStepLog': '流程步骤日志', |
| | | 'menu.flowStepTemplate': '流程步骤模板', |
| | | 'menu.freeze': '库存冻结', |
| | | 'menu.histories': '历史档', |
| | | 'menu.host': '机构管理', |
| | | 'menu.inStatistic': '日入库汇总查询', |
| | | 'menu.inStatisticItem': '日入库明细查询', |
| | | 'menu.inStockPoces': '入库管理', |
| | | 'menu.loc': '库位', |
| | | 'menu.locArea': '逻辑分区(废)', |
| | | 'menu.locAreaMat': '逻辑分区', |
| | | 'menu.locAreaMatRela': '库区物料关系', |
| | | 'menu.locDeadReport': '库存停滞报表', |
| | | 'menu.locItem': '库存明细', |
| | | 'menu.locPreview': '库位明细', |
| | | 'menu.locRevise': '库存调整', |
| | | 'menu.locType': '库位类型(废)', |
| | | 'menu.logs': '日志', |
| | | 'menu.matnr': '物料', |
| | | 'menu.matnrGroup': '物料分组', |
| | | 'menu.matnrRoleMenu': '物料权限', |
| | | 'menu.menu': '菜单管理', |
| | | 'menu.menuPda': 'PDA菜单', |
| | | 'menu.missionFlowStepInstance': '任务流程步骤', |
| | | 'menu.operation': '操作日志', |
| | | 'menu.outBound': '出库作业', |
| | | 'menu.outStatistic': '日出库汇总查询', |
| | | 'menu.outStatisticItem': '日出库明细查询', |
| | | 'menu.outStock': '出库通知单', |
| | | 'menu.outStockItem': '出库单明细', |
| | | 'menu.outStockPoces': '出库管理', |
| | | 'menu.pdaRoleMenu': 'PDA权限', |
| | | 'menu.permissions': '权限管理', |
| | | 'menu.platform': '平台管理', |
| | | 'menu.preparation': '备料单', |
| | | 'menu.purchase': 'PO单', |
| | | 'menu.purchaseItem': 'PO单明细', |
| | | 'menu.qlyInspect': '质检信息', |
| | | 'menu.qlyIsptItem': '质检信息明细', |
| | | 'menu.role': '角色管理', |
| | | 'menu.serialRule': '编码规则', |
| | | 'menu.serialRuleItem': '编码规则子表', |
| | | 'menu.settings': '个人设置', |
| | | 'menu.shipper': '货主信息', |
| | | 'menu.statisticCount': '日出入库汇总统计', |
| | | 'menu.statisticReport': '报表管理', |
| | | 'menu.statistics': '库存查询', |
| | | 'menu.stock': '入出库历史', |
| | | 'menu.stockItem': '单据明细', |
| | | 'menu.stockManage': '库存管理', |
| | | 'menu.stockStatistic': '日入库汇总查询', |
| | | 'menu.stockTransfer': '库位转移', |
| | | 'menu.subsystemFlowTemplate': '子系统流程模板', |
| | | 'menu.supplier': '供应商', |
| | | 'menu.system': '系统设置', |
| | | 'menu.task': '任务管理', |
| | | 'menu.taskInstance': '任务实例', |
| | | 'menu.taskInstanceNode': '任务实例节点', |
| | | 'menu.taskItem': '任务档明细', |
| | | 'menu.taskItemLog': '任务明细历史档', |
| | | 'menu.taskLog': '任务历史档', |
| | | 'menu.taskPathTemplate': '任务路径模板', |
| | | 'menu.taskPathTemplateMerge': '任务路径模板合并', |
| | | 'menu.taskPathTemplateNode': '任务路径模板节点', |
| | | 'menu.tasks': '任务管理', |
| | | 'menu.tenant': '租户管理', |
| | | 'menu.token': '登录日志', |
| | | 'menu.transfer': '调拔单', |
| | | 'menu.transferItem': '调拔单明细', |
| | | 'menu.transferPoces': '调拨管理', |
| | | 'menu.user': '用户管理', |
| | | 'menu.userCenter': '个人中心', |
| | | 'menu.userLogin': '登录日志', |
| | | 'menu.waitPakin': '组托档', |
| | | 'menu.waitPakinItem': '组托档明细', |
| | | 'menu.waitPakinItemLog': '组托历史档明细', |
| | | 'menu.waitPakinLog': '组托历史档', |
| | | 'menu.wareWork': '仓库作业', |
| | | 'menu.warehouse': '仓库', |
| | | 'menu.warehouseAreas': '库区', |
| | | 'menu.warehouseAreasItem': '收货库存', |
| | | 'menu.warehouseRoleMenu': '仓库权限', |
| | | 'menu.warehouseStock': '即时库存', |
| | | 'menu.wave': '波次管理', |
| | | 'menu.waveItem': '波次明细', |
| | | 'menu.waveRule': '波次策略', |
| | | 'menu.whMat': '库区物料关系' |
| | | } |
| | | |
| | | export function resolveBackendMenuTitle(title) { |
| | | if (typeof title !== 'string') { |
| | | return '' |
| | | } |
| | | |
| | | const trimmedTitle = title.trim() |
| | | if (!trimmedTitle) { |
| | | return '' |
| | | } |
| | | |
| | | if (LEGACY_BACKEND_MENU_TITLES[trimmedTitle]) { |
| | | return LEGACY_BACKEND_MENU_TITLES[trimmedTitle] |
| | | } |
| | | |
| | | if (trimmedTitle.startsWith('menus.')) { |
| | | const legacyMenuKey = `menu.${trimmedTitle.slice('menus.'.length)}` |
| | | if (LEGACY_BACKEND_MENU_TITLES[legacyMenuKey]) { |
| | | return LEGACY_BACKEND_MENU_TITLES[legacyMenuKey] |
| | | } |
| | | return trimmedTitle.split('.').pop() || trimmedTitle |
| | | } |
| | | |
| | | if (trimmedTitle.startsWith('menu.')) { |
| | | return trimmedTitle.split('.').pop() || trimmedTitle |
| | | } |
| | | |
| | | return trimmedTitle |
| | | } |
| | |
| | | import { PHASE_1_COMPONENTS } from '../../router/adapters/backendMenuAdapter.js' |
| | | |
| | | const RELEASED_COMPONENT_PATHS = new Set(Object.values(PHASE_1_COMPONENTS)) |
| | | |
| | | function isIframe(url) { |
| | | return url.startsWith('/outside/iframe/') |
| | | } |
| | | |
| | | const isNavigableMenuItem = (menuItem) => { |
| | | if (!menuItem.path || !menuItem.path.trim()) { |
| | | return false |
| | |
| | | const normalizePath = (path) => { |
| | | return path.startsWith('/') ? path : `/${path}` |
| | | } |
| | | |
| | | const hasReleasedComponent = (menuItem) => { |
| | | if (!menuItem || typeof menuItem !== 'object') { |
| | | return false |
| | | } |
| | | const componentPath = typeof menuItem.component === 'string' ? menuItem.component.trim() : '' |
| | | return RELEASED_COMPONENT_PATHS.has(componentPath) |
| | | } |
| | | |
| | | const getFirstMenuPath = (menuList) => { |
| | | if (!Array.isArray(menuList) || menuList.length === 0) { |
| | | return '' |
| | |
| | | return childPath |
| | | } |
| | | } |
| | | return normalizePath(menuItem.path) |
| | | if (hasReleasedComponent(menuItem)) { |
| | | return normalizePath(menuItem.path) |
| | | } |
| | | } |
| | | return '' |
| | | } |
| | |
| | | <!-- 菜单管理页面 --> |
| | | <template> |
| | | <div class="menu-page art-full-height"> |
| | | <!-- 搜索栏 --> |
| | | <ArtSearchBar |
| | | v-model="formFilters" |
| | | :items="formItems" |
| | |
| | | /> |
| | | |
| | | <ElCard class="art-table-card"> |
| | | <!-- 表格头部 --> |
| | | <ArtTableHeader |
| | | :showZebra="false" |
| | | :loading="loading" |
| | |
| | | @refresh="handleRefresh" |
| | | > |
| | | <template #left> |
| | | <ElButton v-auth="'add'" @click="handleAddMenu" v-ripple> 添加菜单 </ElButton> |
| | | <ElButton v-auth="'add'" @click="handleAddMenu" v-ripple>添加菜单</ElButton> |
| | | <ElButton @click="toggleExpand" v-ripple> |
| | | {{ isExpanded ? '收起' : '展开' }} |
| | | </ElButton> |
| | |
| | | |
| | | <ArtTable |
| | | ref="tableRef" |
| | | rowKey="path" |
| | | rowKey="id" |
| | | :loading="loading" |
| | | :columns="columns" |
| | | :data="filteredTableData" |
| | |
| | | :default-expand-all="false" |
| | | /> |
| | | |
| | | <!-- 菜单弹窗 --> |
| | | <MenuDialog |
| | | v-model:visible="dialogVisible" |
| | | :type="dialogType" |
| | | :editData="editData" |
| | | :lockType="lockMenuType" |
| | | :menuTreeOptions="menuTreeOptions" |
| | | @submit="handleSubmit" |
| | | /> |
| | | </ElCard> |
| | |
| | | import ArtSvgIcon from '@/components/core/base/art-svg-icon/index.vue' |
| | | import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue' |
| | | import { useTableColumns } from '@/hooks/core/useTableColumns' |
| | | import { fetchGetMenuList } from '@/api/system-manage' |
| | | import { ElTag, ElMessageBox } from 'element-plus' |
| | | import { |
| | | fetchDeleteMenu, |
| | | fetchGetMenuList, |
| | | fetchSaveMenu, |
| | | fetchUpdateMenu |
| | | } from '@/api/system-manage' |
| | | import { ElTag, ElMessage, ElMessageBox } from 'element-plus' |
| | | |
| | | defineOptions({ name: 'Menus' }) |
| | | |
| | | const loading = ref(false) |
| | | const isExpanded = ref(false) |
| | | const tableRef = ref() |
| | | const dialogVisible = ref(false) |
| | | const dialogType = ref('menu') |
| | | const editData = ref(null) |
| | | const lockMenuType = ref(false) |
| | | const lockMenuType = ref(true) |
| | | const tableData = ref([]) |
| | | const menuTreeOptions = ref([]) |
| | | |
| | | const initialSearchState = { |
| | | name: '', |
| | | route: '' |
| | | } |
| | | |
| | | const formFilters = reactive({ ...initialSearchState }) |
| | | const appliedFilters = reactive({ ...initialSearchState }) |
| | | |
| | | const formItems = computed(() => [ |
| | | { |
| | | label: '菜单名称', |
| | |
| | | props: { clearable: true } |
| | | } |
| | | ]) |
| | | onMounted(() => { |
| | | getMenuList() |
| | | }) |
| | | const getMenuList = async () => { |
| | | |
| | | const normalizeNumber = (value, fallback = 0) => { |
| | | if (value === '' || value === null || value === undefined) { |
| | | return fallback |
| | | } |
| | | const normalized = Number(value) |
| | | return Number.isNaN(normalized) ? fallback : normalized |
| | | } |
| | | |
| | | const normalizeMenuTreeOptions = (nodes = []) => { |
| | | if (!Array.isArray(nodes)) { |
| | | return [] |
| | | } |
| | | |
| | | return nodes |
| | | .map((node) => ({ |
| | | label: formatMenuTitle(node.meta?.title || node.name || ''), |
| | | value: normalizeNumber(node.id, 0), |
| | | children: normalizeMenuTreeOptions(node.children) |
| | | })) |
| | | } |
| | | |
| | | const loadMenuResources = async () => { |
| | | loading.value = true |
| | | try { |
| | | const list = await fetchGetMenuList() |
| | | tableData.value = list |
| | | const list = await fetchGetMenuList({}) |
| | | tableData.value = Array.isArray(list) ? list : [] |
| | | menuTreeOptions.value = [ |
| | | { |
| | | label: '顶级菜单', |
| | | value: 0, |
| | | children: normalizeMenuTreeOptions(tableData.value) |
| | | } |
| | | ] |
| | | } catch (error) { |
| | | throw error instanceof Error ? error : new Error('获取菜单失败') |
| | | ElMessage.error(error?.message || '获取菜单失败') |
| | | } finally { |
| | | loading.value = false |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadMenuResources() |
| | | }) |
| | | |
| | | const hasNestedMenus = (row) => Array.isArray(row.children) && row.children.some((child) => !child.meta?.isAuthButton) |
| | | |
| | | const getMenuTypeTag = (row) => { |
| | | if (row.meta?.isAuthButton) return 'danger' |
| | | if (row.children?.length) return 'info' |
| | | if (row.meta?.link && row.meta?.isIframe) return 'success' |
| | | if (row.path) return 'primary' |
| | | if (row.meta?.link) return 'warning' |
| | | return 'info' |
| | | if (row.meta?.isAuthButton || Number(row.type) === 1) return 'danger' |
| | | if (hasNestedMenus(row)) return 'info' |
| | | return 'primary' |
| | | } |
| | | |
| | | const getMenuTypeText = (row) => { |
| | | if (row.meta?.isAuthButton) return '按钮' |
| | | if (row.children?.length) return '目录' |
| | | if (row.meta?.link && row.meta?.isIframe) return '内嵌' |
| | | if (row.path) return '菜单' |
| | | if (row.meta?.link) return '外链' |
| | | return '未知' |
| | | if (row.meta?.isAuthButton || Number(row.type) === 1) return '按钮' |
| | | if (hasNestedMenus(row)) return '目录' |
| | | return '菜单' |
| | | } |
| | | |
| | | const getStatusMeta = (status) => { |
| | | return normalizeNumber(status, 1) === 1 |
| | | ? { text: '启用', type: 'success' } |
| | | : { text: '禁用', type: 'danger' } |
| | | } |
| | | |
| | | const getMenuDisplayTitle = (row) => { |
| | | const titleKey = row.meta?.title || row.name || '' |
| | | const normalizedTitleKey = |
| | | titleKey && !String(titleKey).includes('.') ? `menu.${titleKey}` : titleKey |
| | | |
| | | return formatMenuTitle(normalizedTitleKey) |
| | | } |
| | | |
| | | const getMenuDisplayIcon = (row) => row.meta?.icon || row.icon || '' |
| | | |
| | | const { columnChecks, columns } = useTableColumns(() => [ |
| | | { |
| | | prop: 'meta.title', |
| | | label: '菜单名称', |
| | | minWidth: 180, |
| | | formatter: (row) => getMenuDisplayTitle(row) |
| | | }, |
| | | { |
| | | prop: 'meta.icon', |
| | | label: '图标预览', |
| | |
| | | |
| | | if (!icon) return h('span', { class: 'text-g-400' }, '-') |
| | | |
| | | return h('div', { class: 'flex items-center justify-center' }, [ |
| | | h(ArtSvgIcon, { icon, class: 'text-base text-g-700' }) |
| | | ]) |
| | | return h( |
| | | 'div', |
| | | { |
| | | class: |
| | | 'mx-auto flex h-8 w-8 items-center justify-center rounded-md border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)]' |
| | | }, |
| | | [h(ArtSvgIcon, { icon, class: 'text-base text-g-700' })] |
| | | ) |
| | | } |
| | | }, |
| | | { |
| | | prop: 'meta.title', |
| | | label: '菜单名称', |
| | | minWidth: 120, |
| | | formatter: (row) => getMenuDisplayTitle(row) |
| | | }, |
| | | { |
| | | prop: 'type', |
| | | label: '菜单类型', |
| | | formatter: (row) => { |
| | | return h(ElTag, { type: getMenuTypeTag(row) }, () => getMenuTypeText(row)) |
| | | } |
| | | width: 110, |
| | | formatter: (row) => |
| | | h(ElTag, { type: getMenuTypeTag(row), effect: 'light' }, () => getMenuTypeText(row)) |
| | | }, |
| | | { |
| | | prop: 'path', |
| | | prop: 'route', |
| | | label: '路由', |
| | | minWidth: 180, |
| | | formatter: (row) => { |
| | | if (row.meta?.isAuthButton) return '' |
| | | return row.meta?.link || row.path || '' |
| | | return row.route || '' |
| | | } |
| | | }, |
| | | { |
| | | prop: 'meta.authList', |
| | | prop: 'authority', |
| | | label: '权限标识', |
| | | minWidth: 180, |
| | | formatter: (row) => { |
| | | if (row.meta?.isAuthButton) { |
| | | return row.meta?.authMark || '' |
| | | return row.authority || row.meta?.authMark || '' |
| | | } |
| | | if (!row.meta?.authList?.length) return '' |
| | | if (!row.meta?.authList?.length) return row.authority || '' |
| | | return `${row.meta.authList.length} 个权限标识` |
| | | } |
| | | }, |
| | | { |
| | | prop: 'date', |
| | | label: '编辑时间', |
| | | formatter: () => '2022-3-12 12:00:00' |
| | | prop: 'sort', |
| | | label: '排序', |
| | | width: 90 |
| | | }, |
| | | { |
| | | prop: 'status', |
| | | label: '状态', |
| | | formatter: () => h(ElTag, { type: 'success' }, () => '启用') |
| | | width: 100, |
| | | formatter: (row) => { |
| | | const statusMeta = getStatusMeta(row.status) |
| | | return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text) |
| | | } |
| | | }, |
| | | { |
| | | prop: 'memo', |
| | | label: '备注', |
| | | minWidth: 180, |
| | | showOverflowTooltip: true, |
| | | formatter: (row) => row.memo || '-' |
| | | }, |
| | | { |
| | | prop: 'operation', |
| | |
| | | width: 180, |
| | | align: 'right', |
| | | formatter: (row) => { |
| | | const buttonStyle = { style: 'text-align: right' } |
| | | const buttonStyle = { class: 'flex justify-end' } |
| | | if (row.meta?.isAuthButton) { |
| | | return h('div', buttonStyle, [ |
| | | h(ArtButtonTable, { |
| | |
| | | }), |
| | | h(ArtButtonTable, { |
| | | type: 'delete', |
| | | onClick: () => handleDeleteAuth() |
| | | onClick: () => handleDeleteAuth(row) |
| | | }) |
| | | ]) |
| | | } |
| | | return h('div', buttonStyle, [ |
| | | h(ArtButtonTable, { |
| | | type: 'add', |
| | | onClick: () => handleAddAuth(), |
| | | onClick: () => handleAddAuth(row), |
| | | title: '新增权限' |
| | | }), |
| | | h(ArtButtonTable, { |
| | |
| | | }), |
| | | h(ArtButtonTable, { |
| | | type: 'delete', |
| | | onClick: () => handleDeleteMenu() |
| | | onClick: () => handleDeleteMenu(row) |
| | | }) |
| | | ]) |
| | | } |
| | | } |
| | | }, |
| | | ]) |
| | | const tableData = ref([]) |
| | | const handleReset = () => { |
| | | Object.assign(formFilters, { ...initialSearchState }) |
| | | Object.assign(appliedFilters, { ...initialSearchState }) |
| | | getMenuList() |
| | | } |
| | | const handleSearch = () => { |
| | | Object.assign(appliedFilters, { ...formFilters }) |
| | | getMenuList() |
| | | } |
| | | const handleRefresh = () => { |
| | | getMenuList() |
| | | } |
| | | |
| | | const deepClone = (obj) => { |
| | | if (obj === null || typeof obj !== 'object') return obj |
| | | if (obj instanceof Date) return new Date(obj) |
| | |
| | | } |
| | | return cloned |
| | | } |
| | | |
| | | const convertAuthListToChildren = (items) => { |
| | | return items.map((item) => { |
| | | const clonedItem = deepClone(item) |
| | |
| | | } |
| | | if (item.meta?.authList?.length) { |
| | | const authChildren = item.meta.authList.map((auth) => ({ |
| | | path: `${item.path}_auth_${auth.authMark}`, |
| | | name: `${String(item.name)}_auth_${auth.authMark}`, |
| | | ...deepClone(auth), |
| | | route: auth.route || '', |
| | | component: auth.component || '', |
| | | meta: { |
| | | title: auth.title, |
| | | authMark: auth.authMark, |
| | | isAuthButton: true, |
| | | parentPath: item.path |
| | | parentPath: item.path, |
| | | icon: auth.icon, |
| | | sort: auth.sort, |
| | | isEnable: normalizeNumber(auth.status, 1) === 1 |
| | | } |
| | | })) |
| | | clonedItem.children = clonedItem.children?.length |
| | |
| | | return clonedItem |
| | | }) |
| | | } |
| | | |
| | | const searchMenu = (items) => { |
| | | const results = [] |
| | | const results = [] |
| | | for (const item of items) { |
| | | const searchName = appliedFilters.name?.toLowerCase().trim() || '' |
| | | const searchRoute = appliedFilters.route?.toLowerCase().trim() || '' |
| | | const menuTitle = getMenuDisplayTitle(item).toLowerCase() |
| | | const menuPath = (item.path || '').toLowerCase() |
| | | const menuRoute = String(item.route || item.path || item.authority || '').toLowerCase() |
| | | const nameMatch = !searchName || menuTitle.includes(searchName) |
| | | const routeMatch = !searchRoute || menuPath.includes(searchRoute) |
| | | const routeMatch = !searchRoute || menuRoute.includes(searchRoute) |
| | | |
| | | if (item.children?.length) { |
| | | const matchedChildren = searchMenu(item.children) |
| | | if (matchedChildren.length > 0) { |
| | |
| | | continue |
| | | } |
| | | } |
| | | |
| | | if (nameMatch && routeMatch) { |
| | | results.push(deepClone(item)) |
| | | } |
| | | } |
| | | return results |
| | | } |
| | | |
| | | const filteredTableData = computed(() => { |
| | | const searchedData = searchMenu(tableData.value) |
| | | return convertAuthListToChildren(searchedData) |
| | | }) |
| | | |
| | | const closeDialog = () => { |
| | | dialogVisible.value = false |
| | | editData.value = null |
| | | } |
| | | |
| | | const handleAddMenu = () => { |
| | | dialogType.value = 'menu' |
| | | editData.value = null |
| | | lockMenuType.value = true |
| | | dialogVisible.value = true |
| | | } |
| | | const handleAddAuth = () => { |
| | | dialogType.value = 'menu' |
| | | editData.value = null |
| | | lockMenuType.value = false |
| | | |
| | | const handleAddAuth = (row) => { |
| | | dialogType.value = 'button' |
| | | editData.value = { |
| | | parentId: row.id, |
| | | type: 1, |
| | | status: 1, |
| | | sort: 0 |
| | | } |
| | | lockMenuType.value = true |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | | const handleEditMenu = (row) => { |
| | | dialogType.value = 'menu' |
| | | editData.value = row |
| | | lockMenuType.value = true |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | | const handleEditAuth = (row) => { |
| | | dialogType.value = 'button' |
| | | editData.value = { |
| | | title: row.meta?.title, |
| | | authMark: row.meta?.authMark |
| | | } |
| | | lockMenuType.value = false |
| | | editData.value = row |
| | | lockMenuType.value = true |
| | | dialogVisible.value = true |
| | | } |
| | | const handleSubmit = (formData) => { |
| | | console.log('提交数据:', formData) |
| | | getMenuList() |
| | | |
| | | const buildMenuSubmitPayload = (formData) => { |
| | | return { |
| | | ...(formData.id ? { id: normalizeNumber(formData.id, 0) } : {}), |
| | | parentId: normalizeNumber(formData.parentId, 0), |
| | | name: String(formData.name || '').trim(), |
| | | route: String(formData.route || '').trim(), |
| | | component: String(formData.component || '').trim(), |
| | | authority: String(formData.authority || '').trim(), |
| | | icon: String(formData.icon || '').trim(), |
| | | sort: normalizeNumber(formData.sort, 0), |
| | | status: normalizeNumber(formData.status, 1), |
| | | memo: String(formData.memo || '').trim(), |
| | | type: formData.menuType === 'button' ? 1 : 0 |
| | | } |
| | | } |
| | | const handleDeleteMenu = async () => { |
| | | |
| | | const handleSubmit = async (formData) => { |
| | | const payload = buildMenuSubmitPayload(formData) |
| | | if (payload.id && payload.id === payload.parentId) { |
| | | ElMessage.error('上级菜单不能选择当前菜单') |
| | | return |
| | | } |
| | | |
| | | try { |
| | | await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', { |
| | | if (payload.id) { |
| | | await fetchUpdateMenu(payload) |
| | | ElMessage.success('修改成功') |
| | | } else { |
| | | await fetchSaveMenu(payload) |
| | | ElMessage.success('新增成功') |
| | | } |
| | | closeDialog() |
| | | await loadMenuResources() |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '提交失败') |
| | | } |
| | | } |
| | | |
| | | const handleDeleteMenu = async (row) => { |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | `确定要删除菜单「${formatMenuTitle(row.meta?.title || row.name || '')}」吗?删除后无法恢复`, |
| | | '删除确认', |
| | | { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | } |
| | | ) |
| | | await fetchDeleteMenu(row.id) |
| | | ElMessage.success('删除成功') |
| | | await loadMenuResources() |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | ElMessage.error(error?.message || '删除失败') |
| | | } |
| | | } |
| | | } |
| | | |
| | | const handleDeleteAuth = async (row) => { |
| | | try { |
| | | await ElMessageBox.confirm(`确定要删除权限「${row.name || row.authority || row.id}」吗?删除后无法恢复`, '删除确认', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }) |
| | | await fetchDeleteMenu(row.id) |
| | | ElMessage.success('删除成功') |
| | | getMenuList() |
| | | await loadMenuResources() |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | ElMessage.error('删除失败') |
| | | ElMessage.error(error?.message || '删除失败') |
| | | } |
| | | } |
| | | } |
| | | const handleDeleteAuth = async () => { |
| | | try { |
| | | await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }) |
| | | ElMessage.success('删除成功') |
| | | getMenuList() |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | ElMessage.error('删除失败') |
| | | } |
| | | } |
| | | |
| | | const handleReset = () => { |
| | | Object.assign(formFilters, { ...initialSearchState }) |
| | | Object.assign(appliedFilters, { ...initialSearchState }) |
| | | loadMenuResources() |
| | | } |
| | | |
| | | const handleSearch = () => { |
| | | Object.assign(appliedFilters, { ...formFilters }) |
| | | } |
| | | |
| | | const handleRefresh = () => { |
| | | loadMenuResources() |
| | | } |
| | | |
| | | const toggleExpand = () => { |
| | | isExpanded.value = !isExpanded.value |
| | | nextTick(() => { |
| | |
| | | :title="dialogTitle" |
| | | :model-value="visible" |
| | | @update:model-value="handleCancel" |
| | | width="860px" |
| | | width="760px" |
| | | align-center |
| | | class="menu-dialog" |
| | | @closed="handleClosed" |
| | |
| | | v-model="form" |
| | | :items="formItems" |
| | | :rules="rules" |
| | | :span="width > 640 ? 12 : 24" |
| | | :span="24" |
| | | :gutter="20" |
| | | label-width="100px" |
| | | :show-reset="false" |
| | |
| | | > |
| | | <template #menuType> |
| | | <ElRadioGroup v-model="form.menuType" :disabled="disableMenuType"> |
| | | <ElRadioButton value="menu" label="menu">菜单</ElRadioButton> |
| | | <ElRadioButton value="button" label="button">按钮</ElRadioButton> |
| | | <ElRadioButton value="menu">菜单</ElRadioButton> |
| | | <ElRadioButton value="button">按钮</ElRadioButton> |
| | | </ElRadioGroup> |
| | | </template> |
| | | </ArtForm> |
| | | |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <ElButton @click="handleCancel">取 消</ElButton> |
| | | <ElButton type="primary" @click="handleSubmit">确 定</ElButton> |
| | | <ElButton @click="handleCancel">取消</ElButton> |
| | | <ElButton type="primary" @click="handleSubmit">确定</ElButton> |
| | | </span> |
| | | </template> |
| | | </ElDialog> |
| | |
| | | <script setup> |
| | | import ArtForm from '@/components/core/forms/art-form/index.vue' |
| | | |
| | | import { ElIcon, ElTooltip } from 'element-plus' |
| | | import { QuestionFilled } from '@element-plus/icons-vue' |
| | | import { formatMenuTitle } from '@/utils/router' |
| | | import { useWindowSize } from '@vueuse/core' |
| | | const { width } = useWindowSize() |
| | | const createLabelTooltip = (label, tooltip) => { |
| | | return () => |
| | | h('span', { class: 'flex items-center' }, [ |
| | | h('span', label), |
| | | h( |
| | | ElTooltip, |
| | | { |
| | | content: tooltip, |
| | | placement: 'top' |
| | | }, |
| | | () => h(ElIcon, { class: 'ml-0.5 cursor-help' }, () => h(QuestionFilled)) |
| | | ) |
| | | ]) |
| | | } |
| | | const createMenuFormState = () => ({ |
| | | menuType: 'menu', |
| | | id: null, |
| | | parentId: 0, |
| | | name: '', |
| | | route: '', |
| | | component: '', |
| | | authority: '', |
| | | icon: '', |
| | | sort: 0, |
| | | status: 1, |
| | | memo: '' |
| | | }) |
| | | |
| | | const props = defineProps({ |
| | | visible: { required: false, default: false }, |
| | | type: { required: false, default: 'menu' }, |
| | | lockType: { required: false, default: false } |
| | | lockType: { required: false, default: false }, |
| | | editData: { required: false, default: null }, |
| | | menuTreeOptions: { required: false, default: () => [] } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:visible', 'submit']) |
| | | const formRef = ref() |
| | | const isEdit = ref(false) |
| | | const form = reactive({ |
| | | menuType: 'menu', |
| | | id: 0, |
| | | name: '', |
| | | path: '', |
| | | label: '', |
| | | component: '', |
| | | icon: '', |
| | | isEnable: true, |
| | | sort: 1, |
| | | isMenu: true, |
| | | keepAlive: true, |
| | | isHide: false, |
| | | isHideTab: false, |
| | | link: '', |
| | | isIframe: false, |
| | | showBadge: false, |
| | | showTextBadge: '', |
| | | fixedTab: false, |
| | | activePath: '', |
| | | roles: [], |
| | | isFullPage: false, |
| | | authName: '', |
| | | authLabel: '', |
| | | authIcon: '', |
| | | authSort: 1 |
| | | }) |
| | | const rules = reactive({ |
| | | name: [ |
| | | { required: true, message: '请输入菜单名称', trigger: 'blur' }, |
| | | { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' } |
| | | ], |
| | | path: [{ required: true, message: '请输入路由地址', trigger: 'blur' }], |
| | | label: [{ required: true, message: '输入权限标识', trigger: 'blur' }], |
| | | authName: [{ required: true, message: '请输入权限名称', trigger: 'blur' }], |
| | | authLabel: [{ required: true, message: '请输入权限标识', trigger: 'blur' }] |
| | | }) |
| | | const form = reactive(createMenuFormState()) |
| | | |
| | | const isEdit = computed(() => Boolean(form.id)) |
| | | const dialogTitle = computed(() => `${isEdit.value ? '编辑' : '新建'}${form.menuType === 'button' ? '按钮' : '菜单'}`) |
| | | const disableMenuType = computed(() => props.lockType || isEdit.value) |
| | | |
| | | const rules = computed(() => ({ |
| | | name: [{ required: true, message: form.menuType === 'button' ? '请输入权限名称' : '请输入菜单名称', trigger: 'blur' }], |
| | | route: |
| | | form.menuType === 'menu' |
| | | ? [{ required: true, message: '请输入路由地址', trigger: 'blur' }] |
| | | : [], |
| | | authority: |
| | | form.menuType === 'button' |
| | | ? [{ required: true, message: '请输入权限标识', trigger: 'blur' }] |
| | | : [] |
| | | })) |
| | | |
| | | const formItems = computed(() => { |
| | | const baseItems = [{ label: '菜单类型', key: 'menuType', span: 24 }] |
| | | const switchSpan = width.value < 640 ? 12 : 6 |
| | | const items = [ |
| | | { label: '菜单类型', key: 'menuType', span: 24 }, |
| | | { |
| | | label: '上级菜单', |
| | | key: 'parentId', |
| | | type: 'treeselect', |
| | | span: 24, |
| | | props: { |
| | | data: props.menuTreeOptions, |
| | | props: { |
| | | label: 'label', |
| | | value: 'value', |
| | | children: 'children' |
| | | }, |
| | | placeholder: '请选择上级菜单', |
| | | checkStrictly: true, |
| | | clearable: false, |
| | | defaultExpandAll: true |
| | | } |
| | | }, |
| | | { |
| | | label: form.menuType === 'button' ? '权限名称' : '菜单名称', |
| | | key: 'name', |
| | | type: 'input', |
| | | span: 24, |
| | | props: { |
| | | placeholder: form.menuType === 'button' ? '请输入权限名称' : '请输入菜单名称', |
| | | clearable: true |
| | | } |
| | | } |
| | | ] |
| | | |
| | | if (form.menuType === 'menu') { |
| | | return [ |
| | | ...baseItems, |
| | | { label: '菜单名称', key: 'name', type: 'input', props: { placeholder: '菜单名称' } }, |
| | | items.push( |
| | | { |
| | | label: createLabelTooltip( |
| | | '路由地址', |
| | | '一级菜单:以 / 开头的绝对路径(如 /dashboard)\n二级及以下:相对路径(如 console、user)' |
| | | ), |
| | | key: 'path', |
| | | label: '路由地址', |
| | | key: 'route', |
| | | type: 'input', |
| | | props: { placeholder: '如:/dashboard 或 console' } |
| | | span: 24, |
| | | props: { |
| | | placeholder: '请输入路由地址', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { label: '权限标识', key: 'label', type: 'input', props: { placeholder: '如:User' } }, |
| | | { |
| | | label: createLabelTooltip( |
| | | '组件路径', |
| | | '一级父级菜单:填写 /index/index\n具体页面:填写组件路径(如 /system/user)\n目录菜单:留空' |
| | | ), |
| | | label: '组件标识', |
| | | key: 'component', |
| | | type: 'input', |
| | | props: { placeholder: '如:/system/user 或留空' } |
| | | }, |
| | | { label: '图标', key: 'icon', type: 'input', props: { placeholder: '如:ri:user-line' } }, |
| | | { |
| | | label: createLabelTooltip( |
| | | '角色权限', |
| | | '仅用于前端权限模式:配置角色标识(如 R_SUPER、R_ADMIN)\n后端权限模式:无需配置' |
| | | ), |
| | | key: 'roles', |
| | | type: 'inputtag', |
| | | props: { placeholder: '输入角色标识后按回车,如:R_SUPER' } |
| | | }, |
| | | { |
| | | label: '菜单排序', |
| | | key: 'sort', |
| | | type: 'number', |
| | | props: { min: 1, controlsPosition: 'right', style: { width: '100%' } } |
| | | }, |
| | | { |
| | | label: '外部链接', |
| | | key: 'link', |
| | | type: 'input', |
| | | props: { placeholder: '如:https://www.example.com' } |
| | | }, |
| | | { |
| | | label: '文本徽章', |
| | | key: 'showTextBadge', |
| | | type: 'input', |
| | | props: { placeholder: '如:New、Hot' } |
| | | }, |
| | | { |
| | | label: createLabelTooltip( |
| | | '激活路径', |
| | | '用于详情页等隐藏菜单,指定高亮显示的父级菜单路径\n例如:用户详情页高亮显示"用户管理"菜单' |
| | | ), |
| | | key: 'activePath', |
| | | type: 'input', |
| | | props: { placeholder: '如:/system/user' } |
| | | }, |
| | | { label: '是否启用', key: 'isEnable', type: 'switch', span: switchSpan }, |
| | | { label: '页面缓存', key: 'keepAlive', type: 'switch', span: switchSpan }, |
| | | { label: '隐藏菜单', key: 'isHide', type: 'switch', span: switchSpan }, |
| | | { label: '是否内嵌', key: 'isIframe', type: 'switch', span: switchSpan }, |
| | | { label: '显示徽章', key: 'showBadge', type: 'switch', span: switchSpan }, |
| | | { label: '固定标签', key: 'fixedTab', type: 'switch', span: switchSpan }, |
| | | { label: '标签隐藏', key: 'isHideTab', type: 'switch', span: switchSpan }, |
| | | { label: '全屏页面', key: 'isFullPage', type: 'switch', span: switchSpan } |
| | | ] |
| | | } else { |
| | | return [ |
| | | ...baseItems, |
| | | { |
| | | label: '权限名称', |
| | | key: 'authName', |
| | | type: 'input', |
| | | props: { placeholder: '如:新增、编辑、删除' } |
| | | }, |
| | | { |
| | | label: '权限标识', |
| | | key: 'authLabel', |
| | | type: 'input', |
| | | props: { placeholder: '如:add、edit、delete' } |
| | | }, |
| | | { |
| | | label: '权限排序', |
| | | key: 'authSort', |
| | | type: 'number', |
| | | props: { min: 1, controlsPosition: 'right', style: { width: '100%' } } |
| | | span: 24, |
| | | props: { |
| | | placeholder: '请输入组件标识', |
| | | clearable: true |
| | | } |
| | | } |
| | | ] |
| | | ) |
| | | } |
| | | |
| | | items.push( |
| | | { |
| | | label: '权限标识', |
| | | key: 'authority', |
| | | type: 'input', |
| | | span: 24, |
| | | props: { |
| | | placeholder: '请输入权限标识', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '图标', |
| | | key: 'icon', |
| | | type: 'input', |
| | | span: 24, |
| | | props: { |
| | | placeholder: '请输入图标名称', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '排序', |
| | | key: 'sort', |
| | | type: 'number', |
| | | span: 24, |
| | | props: { |
| | | min: 0, |
| | | controlsPosition: 'right', |
| | | style: { width: '100%' } |
| | | } |
| | | }, |
| | | { |
| | | label: '状态', |
| | | key: 'status', |
| | | type: 'select', |
| | | span: 24, |
| | | props: { |
| | | placeholder: '请选择状态', |
| | | options: [ |
| | | { label: '启用', value: 1 }, |
| | | { label: '禁用', value: 0 } |
| | | ] |
| | | } |
| | | }, |
| | | { |
| | | label: '备注', |
| | | key: 'memo', |
| | | type: 'input', |
| | | span: 24, |
| | | props: { |
| | | type: 'textarea', |
| | | rows: 3, |
| | | placeholder: '请输入备注', |
| | | clearable: true |
| | | } |
| | | } |
| | | ) |
| | | |
| | | return items |
| | | }) |
| | | const dialogTitle = computed(() => { |
| | | const type = form.menuType === 'menu' ? '菜单' : '按钮' |
| | | return isEdit.value ? `编辑${type}` : `新建${type}` |
| | | }) |
| | | const disableMenuType = computed(() => { |
| | | if (isEdit.value) return true |
| | | if (!isEdit.value && form.menuType === 'menu' && props.lockType) return true |
| | | return false |
| | | }) |
| | | |
| | | const normalizeNumber = (value, fallback = 0) => { |
| | | if (value === '' || value === null || value === undefined) { |
| | | return fallback |
| | | } |
| | | const normalized = Number(value) |
| | | return Number.isNaN(normalized) ? fallback : normalized |
| | | } |
| | | |
| | | const resetForm = () => { |
| | | formRef.value?.reset() |
| | | form.menuType = 'menu' |
| | | Object.assign(form, createMenuFormState()) |
| | | formRef.value?.clearValidate?.() |
| | | } |
| | | |
| | | const loadFormData = () => { |
| | | if (!props.editData) return |
| | | isEdit.value = true |
| | | if (form.menuType === 'menu') { |
| | | const row = props.editData |
| | | form.id = row.id || 0 |
| | | form.name = formatMenuTitle(row.meta?.title || '') |
| | | form.path = row.path || '' |
| | | form.label = row.name || '' |
| | | form.component = row.component || '' |
| | | form.icon = row.meta?.icon || '' |
| | | form.sort = row.meta?.sort || 1 |
| | | form.isMenu = row.meta?.isMenu ?? true |
| | | form.keepAlive = row.meta?.keepAlive ?? false |
| | | form.isHide = row.meta?.isHide ?? false |
| | | form.isHideTab = row.meta?.isHideTab ?? false |
| | | form.isEnable = row.meta?.isEnable ?? true |
| | | form.link = row.meta?.link || '' |
| | | form.isIframe = row.meta?.isIframe ?? false |
| | | form.showBadge = row.meta?.showBadge ?? false |
| | | form.showTextBadge = row.meta?.showTextBadge || '' |
| | | form.fixedTab = row.meta?.fixedTab ?? false |
| | | form.activePath = row.meta?.activePath || '' |
| | | form.roles = row.meta?.roles || [] |
| | | form.isFullPage = row.meta?.isFullPage ?? false |
| | | } else { |
| | | const row = props.editData |
| | | form.authName = row.title || '' |
| | | form.authLabel = row.authMark || '' |
| | | form.authIcon = row.icon || '' |
| | | form.authSort = row.sort || 1 |
| | | resetForm() |
| | | form.menuType = props.type || 'menu' |
| | | |
| | | const row = props.editData |
| | | if (!row || typeof row !== 'object') { |
| | | return |
| | | } |
| | | |
| | | form.menuType = Number(row.type) === 1 ? 'button' : props.type || 'menu' |
| | | form.id = row.id ?? null |
| | | form.parentId = normalizeNumber(row.parentId, 0) |
| | | form.name = row.name || '' |
| | | form.route = row.route || '' |
| | | form.component = row.component || '' |
| | | form.authority = row.authority || row.meta?.authMark || '' |
| | | form.icon = row.icon || row.meta?.icon || '' |
| | | form.sort = normalizeNumber(row.sort ?? row.meta?.sort, 0) |
| | | form.status = normalizeNumber(row.status, row.meta?.isEnable === false ? 0 : 1) |
| | | form.memo = row.memo || '' |
| | | } |
| | | |
| | | const handleSubmit = async () => { |
| | | if (!formRef.value) return |
| | | try { |
| | | await formRef.value.validate() |
| | | emit('submit', { ...form }) |
| | | ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`) |
| | | handleCancel() |
| | | emit('submit', { |
| | | ...form, |
| | | type: form.menuType === 'button' ? 1 : 0 |
| | | }) |
| | | } catch { |
| | | ElMessage.error('表单校验失败,请检查输入') |
| | | return |
| | | } |
| | | } |
| | | |
| | | const handleCancel = () => { |
| | | emit('update:visible', false) |
| | | } |
| | | |
| | | const handleClosed = () => { |
| | | resetForm() |
| | | isEdit.value = false |
| | | } |
| | | |
| | | watch( |
| | | () => props.visible, |
| | | (newVal) => { |
| | | if (newVal) { |
| | | form.menuType = props.type |
| | | (visible) => { |
| | | if (visible) { |
| | | loadFormData() |
| | | nextTick(() => { |
| | | if (props.editData) { |
| | | loadFormData() |
| | | } |
| | | formRef.value?.clearValidate?.() |
| | | }) |
| | | } |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.editData, |
| | | () => { |
| | | if (props.visible) { |
| | | loadFormData() |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.type, |
| | | (newType) => { |
| | | () => { |
| | | if (props.visible) { |
| | | form.menuType = newType |
| | | loadFormData() |
| | | } |
| | | } |
| | | ) |
| | |
| | | <template> |
| | | <ElDialog |
| | | v-model="visible" |
| | | :title="dialogType === 'add' ? '新增角色' : '编辑角色'" |
| | | width="30%" |
| | | :title="dialogTitle" |
| | | :model-value="visible" |
| | | width="720px" |
| | | align-center |
| | | @close="handleClose" |
| | | @update:model-value="handleCancel" |
| | | @closed="handleClosed" |
| | | > |
| | | <ElForm ref="formRef" :model="form" :rules="rules" label-width="120px"> |
| | | <ElFormItem label="角色名称" prop="roleName"> |
| | | <ElInput v-model="form.roleName" placeholder="请输入角色名称" /> |
| | | </ElFormItem> |
| | | <ElFormItem label="角色编码" prop="roleCode"> |
| | | <ElInput v-model="form.roleCode" placeholder="请输入角色编码" /> |
| | | </ElFormItem> |
| | | <ElFormItem label="描述" prop="description"> |
| | | <ElInput |
| | | v-model="form.description" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请输入角色描述" |
| | | /> |
| | | </ElFormItem> |
| | | <ElFormItem label="启用"> |
| | | <ElSwitch v-model="form.enabled" /> |
| | | </ElFormItem> |
| | | </ElForm> |
| | | <ArtForm |
| | | ref="formRef" |
| | | v-model="form" |
| | | :items="formItems" |
| | | :rules="rules" |
| | | :span="12" |
| | | :gutter="20" |
| | | label-width="110px" |
| | | :show-reset="false" |
| | | :show-submit="false" |
| | | /> |
| | | |
| | | <template #footer> |
| | | <ElButton @click="handleClose">取消</ElButton> |
| | | <ElButton type="primary" @click="handleSubmit">提交</ElButton> |
| | | <span class="dialog-footer"> |
| | | <ElButton @click="handleCancel">取消</ElButton> |
| | | <ElButton type="primary" @click="handleSubmit">确定</ElButton> |
| | | </span> |
| | | </template> |
| | | </ElDialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import ArtForm from '@/components/core/forms/art-form/index.vue' |
| | | import { buildRoleDialogModel, createRoleFormState } from '../rolePage.helpers' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { required: false, default: false }, |
| | | visible: { required: false, default: false }, |
| | | dialogType: { required: false, default: 'add' }, |
| | | roleData: { required: false, default: void 0 } |
| | | roleData: { required: false, default: () => ({}) } |
| | | }) |
| | | const emit = defineEmits(['update:modelValue', 'success']) |
| | | |
| | | const emit = defineEmits(['update:visible', 'submit']) |
| | | const formRef = ref() |
| | | const visible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (value) => emit('update:modelValue', value) |
| | | }) |
| | | const rules = reactive({ |
| | | roleName: [ |
| | | { required: true, message: '请输入角色名称', trigger: 'blur' }, |
| | | { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' } |
| | | ], |
| | | roleCode: [ |
| | | { required: true, message: '请输入角色编码', trigger: 'blur' }, |
| | | { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' } |
| | | ], |
| | | description: [{ required: true, message: '请输入角色描述', trigger: 'blur' }] |
| | | }) |
| | | const form = reactive({ |
| | | roleId: 0, |
| | | roleName: '', |
| | | roleCode: '', |
| | | description: '', |
| | | createTime: '', |
| | | enabled: true |
| | | }) |
| | | watch( |
| | | () => props.modelValue, |
| | | (newVal) => { |
| | | if (newVal) initForm() |
| | | } |
| | | ) |
| | | watch( |
| | | () => props.roleData, |
| | | (newData) => { |
| | | if (newData && props.modelValue) initForm() |
| | | const form = reactive(createRoleFormState()) |
| | | |
| | | const isEdit = computed(() => props.dialogType === 'edit') |
| | | const dialogTitle = computed(() => (isEdit.value ? '编辑角色' : '新增角色')) |
| | | |
| | | const rules = computed(() => ({ |
| | | name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }] |
| | | })) |
| | | |
| | | const formItems = computed(() => [ |
| | | { |
| | | label: '角色名称', |
| | | key: 'name', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '请输入角色名称', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | const initForm = () => { |
| | | if (props.dialogType === 'edit' && props.roleData) { |
| | | Object.assign(form, props.roleData) |
| | | } else { |
| | | Object.assign(form, { |
| | | roleId: 0, |
| | | roleName: '', |
| | | roleCode: '', |
| | | description: '', |
| | | createTime: '', |
| | | enabled: true |
| | | }) |
| | | { |
| | | label: '角色编码', |
| | | key: 'code', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '请输入角色编码', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '状态', |
| | | key: 'status', |
| | | type: 'select', |
| | | props: { |
| | | placeholder: '请选择状态', |
| | | clearable: true, |
| | | options: [ |
| | | { label: '正常', value: 1 }, |
| | | { label: '禁用', value: 0 } |
| | | ] |
| | | } |
| | | }, |
| | | { |
| | | label: '备注', |
| | | key: 'memo', |
| | | type: 'input', |
| | | span: 24, |
| | | props: { |
| | | type: 'textarea', |
| | | rows: 3, |
| | | placeholder: '请输入备注', |
| | | clearable: true |
| | | } |
| | | } |
| | | ]) |
| | | |
| | | const resetForm = () => { |
| | | Object.assign(form, createRoleFormState()) |
| | | formRef.value?.clearValidate?.() |
| | | } |
| | | const handleClose = () => { |
| | | visible.value = false |
| | | formRef.value?.resetFields() |
| | | |
| | | const loadFormData = () => { |
| | | Object.assign(form, buildRoleDialogModel(props.roleData)) |
| | | } |
| | | |
| | | const handleSubmit = async () => { |
| | | if (!formRef.value) return |
| | | try { |
| | | await formRef.value.validate() |
| | | const message = props.dialogType === 'add' ? '新增成功' : '修改成功' |
| | | ElMessage.success(message) |
| | | emit('success') |
| | | handleClose() |
| | | } catch (error) { |
| | | console.log('表单验证失败:', error) |
| | | emit('submit', { ...form }) |
| | | } catch { |
| | | return |
| | | } |
| | | } |
| | | |
| | | const handleCancel = () => { |
| | | emit('update:visible', false) |
| | | } |
| | | |
| | | const handleClosed = () => { |
| | | resetForm() |
| | | } |
| | | |
| | | watch( |
| | | () => props.visible, |
| | | (visible) => { |
| | | if (visible) { |
| | | loadFormData() |
| | | nextTick(() => { |
| | | formRef.value?.clearValidate?.() |
| | | }) |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.roleData, |
| | | () => { |
| | | if (props.visible) { |
| | | loadFormData() |
| | | } |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | </script> |
| | |
| | | <template> |
| | | <ElDialog |
| | | v-model="visible" |
| | | title="菜单权限" |
| | | width="520px" |
| | | align-center |
| | | class="el-dialog-border" |
| | | @close="handleClose" |
| | | <ElDrawer |
| | | :model-value="visible" |
| | | title="角色权限" |
| | | size="860px" |
| | | destroy-on-close |
| | | @update:model-value="handleVisibleChange" |
| | | @closed="handleClosed" |
| | | > |
| | | <ElScrollbar height="70vh"> |
| | | <ElTree |
| | | ref="treeRef" |
| | | :data="processedMenuList" |
| | | show-checkbox |
| | | node-key="name" |
| | | :default-expand-all="isExpandAll" |
| | | :default-checked-keys="[1, 2, 3]" |
| | | :props="defaultProps" |
| | | @check="handleTreeCheck" |
| | | > |
| | | <template #default="{ data }"> |
| | | <div style="display: flex; align-items: center"> |
| | | <span v-if="data.isAuth"> |
| | | {{ data.label }} |
| | | </span> |
| | | <span v-else>{{ defaultProps.label(data) }}</span> |
| | | </div> |
| | | </template> |
| | | </ElTree> |
| | | </ElScrollbar> |
| | | <template #footer> |
| | | <ElButton @click="outputSelectedData" style="margin-left: 8px">获取选中数据</ElButton> |
| | | <div class="mb-4 text-sm text-[var(--art-text-secondary)]"> |
| | | 当前角色:{{ roleLabel }} |
| | | </div> |
| | | |
| | | <ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton> |
| | | <ElButton @click="toggleSelectAll" style="margin-left: 8px">{{ |
| | | isSelectAll ? '取消全选' : '全部选择' |
| | | }}</ElButton> |
| | | <ElButton type="primary" @click="savePermission">保存</ElButton> |
| | | </template> |
| | | </ElDialog> |
| | | <ElTabs v-model="activeScopeType" class="role-scope-tabs"> |
| | | <ElTabPane |
| | | v-for="config in scopeConfigs" |
| | | :key="config.scopeType" |
| | | :label="config.title" |
| | | :name="config.scopeType" |
| | | > |
| | | <div v-if="scopeState[config.scopeType].loading" class="py-6"> |
| | | <ElSkeleton :rows="10" animated /> |
| | | </div> |
| | | <div v-else class="space-y-3"> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <ElSpace wrap> |
| | | <ElButton @click="handleSelectAll(config.scopeType)">全选</ElButton> |
| | | <ElButton @click="handleClear(config.scopeType)">清空</ElButton> |
| | | </ElSpace> |
| | | <ElButton type="primary" @click="handleSave(config.scopeType)">保存当前权限</ElButton> |
| | | </div> |
| | | |
| | | <div class="flex items-center gap-3"> |
| | | <ElInput |
| | | v-model.trim="scopeState[config.scopeType].condition" |
| | | clearable |
| | | placeholder="搜索权限树" |
| | | @clear="handleSearch(config.scopeType)" |
| | | @keyup.enter="handleSearch(config.scopeType)" |
| | | /> |
| | | <ElButton @click="handleSearch(config.scopeType)">搜索</ElButton> |
| | | </div> |
| | | |
| | | <ElScrollbar height="56vh"> |
| | | <ElTree |
| | | :ref="(el) => setTreeRef(config.scopeType, el)" |
| | | :data="scopeState[config.scopeType].treeData" |
| | | node-key="id" |
| | | show-checkbox |
| | | :default-expand-all="true" |
| | | :default-checked-keys="scopeState[config.scopeType].checkedKeys" |
| | | :props="treeProps" |
| | | @check="handleTreeCheck(config.scopeType)" |
| | | > |
| | | <template #default="{ data }"> |
| | | <div class="flex items-center gap-2"> |
| | | <span>{{ resolveScopeNodeLabel(data) }}</span> |
| | | <ElTag v-if="data.isAuthButton" type="info" effect="plain" size="small"> |
| | | 按钮 |
| | | </ElTag> |
| | | </div> |
| | | </template> |
| | | </ElTree> |
| | | </ElScrollbar> |
| | | </div> |
| | | </ElTabPane> |
| | | </ElTabs> |
| | | </ElDrawer> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { useMenuStore } from '@/store/modules/menu' |
| | | import { formatMenuTitle } from '@/utils/router' |
| | | import { |
| | | buildRoleScopeSubmitPayload, |
| | | getRoleScopeConfig, |
| | | normalizeRoleScopeTreeData |
| | | } from '../rolePage.helpers' |
| | | import { fetchGetRoleScopeList, fetchGetRoleScopeTree, fetchUpdateRoleScope } from '@/api/system-manage' |
| | | import { resolveBackendMenuTitle } from '@/utils/backend-menu-title' |
| | | import { ElMessage } from 'element-plus' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { required: false, default: false }, |
| | | roleData: { required: false, default: void 0 } |
| | | visible: { required: false, default: false }, |
| | | roleData: { required: false, default: () => ({}) }, |
| | | scopeType: { required: false, default: 'menu' } |
| | | }) |
| | | const emit = defineEmits(['update:modelValue', 'success']) |
| | | const { menuList } = storeToRefs(useMenuStore()) |
| | | const treeRef = ref() |
| | | const isExpandAll = ref(true) |
| | | const isSelectAll = ref(false) |
| | | const visible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (value) => emit('update:modelValue', value) |
| | | }) |
| | | const processedMenuList = computed(() => { |
| | | const processNode = (node) => { |
| | | const processed = { ...node } |
| | | if (node.meta?.authList?.length) { |
| | | const authNodes = node.meta.authList.map((auth) => ({ |
| | | id: `${node.id}_${auth.authMark}`, |
| | | name: `${node.name}_${auth.authMark}`, |
| | | label: auth.title, |
| | | authMark: auth.authMark, |
| | | isAuth: true, |
| | | checked: auth.checked || false |
| | | })) |
| | | processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes |
| | | } |
| | | if (processed.children) { |
| | | processed.children = processed.children.map(processNode) |
| | | } |
| | | return processed |
| | | } |
| | | return menuList.value.map(processNode) |
| | | }) |
| | | const defaultProps = { |
| | | children: 'children', |
| | | label: (data) => formatMenuTitle(data.meta?.title) || data.label || '' |
| | | |
| | | const emit = defineEmits(['update:visible', 'success']) |
| | | |
| | | const scopeConfigs = ['menu', 'pda', 'matnr', 'warehouse'].map((scopeType) => getRoleScopeConfig(scopeType)) |
| | | const activeScopeType = ref(props.scopeType || 'menu') |
| | | const treeRefs = reactive({}) |
| | | const treeProps = { |
| | | label: 'label', |
| | | children: 'children' |
| | | } |
| | | watch( |
| | | () => props.modelValue, |
| | | (newVal) => { |
| | | if (newVal && props.roleData) { |
| | | console.log('设置权限:', props.roleData) |
| | | } |
| | | } |
| | | const scopeState = reactive( |
| | | Object.fromEntries( |
| | | scopeConfigs.map((config) => [ |
| | | config.scopeType, |
| | | { |
| | | loading: false, |
| | | loaded: false, |
| | | treeData: [], |
| | | checkedKeys: [], |
| | | halfCheckedKeys: [], |
| | | condition: '' |
| | | } |
| | | ]) |
| | | ) |
| | | ) |
| | | const handleClose = () => { |
| | | visible.value = false |
| | | treeRef.value?.setCheckedKeys([]) |
| | | } |
| | | const savePermission = () => { |
| | | ElMessage.success('权限保存成功') |
| | | emit('success') |
| | | handleClose() |
| | | } |
| | | const toggleExpandAll = () => { |
| | | const tree = treeRef.value |
| | | if (!tree) return |
| | | const nodes = tree.store.nodesMap |
| | | Object.values(nodes).forEach((node) => { |
| | | node.expanded = !isExpandAll.value |
| | | }) |
| | | isExpandAll.value = !isExpandAll.value |
| | | } |
| | | const toggleSelectAll = () => { |
| | | const tree = treeRef.value |
| | | if (!tree) return |
| | | if (!isSelectAll.value) { |
| | | const allKeys = getAllNodeKeys(processedMenuList.value) |
| | | tree.setCheckedKeys(allKeys) |
| | | } else { |
| | | tree.setCheckedKeys([]) |
| | | |
| | | const visible = computed({ |
| | | get: () => props.visible, |
| | | set: (value) => emit('update:visible', value) |
| | | }) |
| | | |
| | | const roleLabel = computed(() => props.roleData?.name || props.roleData?.code || '未选择角色') |
| | | |
| | | const loadScopeData = async (scopeType, { reloadSelection = true } = {}) => { |
| | | const config = getRoleScopeConfig(scopeType) |
| | | const state = scopeState[scopeType] |
| | | state.loading = true |
| | | try { |
| | | const requests = [fetchGetRoleScopeTree(config.scopeType, { condition: state.condition || '' })] |
| | | if (reloadSelection) { |
| | | requests.unshift(fetchGetRoleScopeList(config.scopeType, props.roleData.id)) |
| | | } |
| | | |
| | | const [checkedIds, treeData] = reloadSelection ? await Promise.all(requests) : [state.checkedKeys, await requests[0]] |
| | | state.treeData = normalizeRoleScopeTreeData(config.scopeType, treeData) |
| | | state.checkedKeys = normalizeScopeKeys(checkedIds) |
| | | state.halfCheckedKeys = [] |
| | | state.loaded = true |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || `加载${config.title}失败`) |
| | | } finally { |
| | | state.loading = false |
| | | nextTick(() => { |
| | | treeRefs[scopeType]?.setCheckedKeys(scopeState[scopeType].checkedKeys) |
| | | }) |
| | | } |
| | | isSelectAll.value = !isSelectAll.value |
| | | } |
| | | |
| | | const ensureScopeLoaded = async (scopeType, options = {}) => { |
| | | if (!props.roleData?.id || !scopeType) { |
| | | return |
| | | } |
| | | |
| | | const { force = false, reloadSelection = true } = options |
| | | if (!force && scopeState[scopeType].loaded) { |
| | | return |
| | | } |
| | | |
| | | await loadScopeData(scopeType, { reloadSelection }) |
| | | } |
| | | |
| | | const normalizeScopeKeys = (keys = []) => { |
| | | if (!Array.isArray(keys)) { |
| | | return [] |
| | | } |
| | | |
| | | return Array.from( |
| | | new Set( |
| | | keys |
| | | .map((key) => normalizeScopeKey(key)) |
| | | .filter((key) => key !== '') |
| | | ) |
| | | ) |
| | | } |
| | | |
| | | const normalizeScopeKey = (value) => { |
| | | if (value === '' || value === null || value === void 0) { |
| | | return '' |
| | | } |
| | | const numeric = Number(value) |
| | | if (Number.isNaN(numeric)) { |
| | | return String(value) |
| | | } |
| | | return String(numeric) |
| | | } |
| | | |
| | | const setTreeRef = (scopeType, el) => { |
| | | if (el) { |
| | | treeRefs[scopeType] = el |
| | | } |
| | | } |
| | | |
| | | const handleTreeCheck = (scopeType) => { |
| | | const tree = treeRefs[scopeType] |
| | | if (!tree) return |
| | | scopeState[scopeType].checkedKeys = normalizeScopeKeys(tree.getCheckedKeys()) |
| | | scopeState[scopeType].halfCheckedKeys = normalizeScopeKeys(tree.getHalfCheckedKeys()) |
| | | } |
| | | |
| | | const handleSelectAll = (scopeType) => { |
| | | const tree = treeRefs[scopeType] |
| | | if (!tree) return |
| | | const allKeys = getAllNodeKeys(scopeState[scopeType].treeData) |
| | | tree.setCheckedKeys(allKeys) |
| | | handleTreeCheck(scopeType) |
| | | } |
| | | |
| | | const handleClear = (scopeType) => { |
| | | const tree = treeRefs[scopeType] |
| | | if (!tree) return |
| | | tree.setCheckedKeys([]) |
| | | handleTreeCheck(scopeType) |
| | | } |
| | | |
| | | const handleSave = async (scopeType) => { |
| | | if (!props.roleData?.id) return |
| | | try { |
| | | await fetchUpdateRoleScope( |
| | | scopeType, |
| | | buildRoleScopeSubmitPayload( |
| | | props.roleData.id, |
| | | scopeState[scopeType].checkedKeys, |
| | | scopeState[scopeType].halfCheckedKeys |
| | | ) |
| | | ) |
| | | ElMessage.success('权限保存成功') |
| | | emit('success') |
| | | visible.value = false |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '权限保存失败') |
| | | } |
| | | } |
| | | |
| | | const handleVisibleChange = (value) => { |
| | | visible.value = value |
| | | } |
| | | |
| | | const handleSearch = async (scopeType) => { |
| | | await loadScopeData(scopeType, { reloadSelection: false }) |
| | | } |
| | | |
| | | const resolveScopeNodeLabel = (data) => { |
| | | const rawLabel = typeof data?.label === 'string' ? data.label.trim() : '' |
| | | if (!rawLabel) { |
| | | return '' |
| | | } |
| | | return resolveBackendMenuTitle(rawLabel) |
| | | } |
| | | |
| | | const handleClosed = () => { |
| | | activeScopeType.value = props.scopeType || 'menu' |
| | | Object.keys(scopeState).forEach((key) => { |
| | | scopeState[key].loading = false |
| | | scopeState[key].loaded = false |
| | | scopeState[key].treeData = [] |
| | | scopeState[key].checkedKeys = [] |
| | | scopeState[key].halfCheckedKeys = [] |
| | | scopeState[key].condition = '' |
| | | }) |
| | | } |
| | | |
| | | const getAllNodeKeys = (nodes) => { |
| | | const keys = [] |
| | | const traverse = (nodeList) => { |
| | | nodeList.forEach((node) => { |
| | | if (node.name) keys.push(node.name) |
| | | if (node.children?.length) traverse(node.children) |
| | | if (node.id !== void 0 && node.id !== null && node.id !== '') { |
| | | keys.push(String(node.id)) |
| | | } |
| | | if (node.children?.length) { |
| | | traverse(node.children) |
| | | } |
| | | }) |
| | | } |
| | | traverse(nodes) |
| | | traverse(Array.isArray(nodes) ? nodes : []) |
| | | return keys |
| | | } |
| | | const handleTreeCheck = () => { |
| | | const tree = treeRef.value |
| | | if (!tree) return |
| | | const checkedKeys = tree.getCheckedKeys() |
| | | const allKeys = getAllNodeKeys(processedMenuList.value) |
| | | isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0 |
| | | } |
| | | const outputSelectedData = () => { |
| | | const tree = treeRef.value |
| | | if (!tree) return |
| | | const selectedData = { |
| | | checkedKeys: tree.getCheckedKeys(), |
| | | halfCheckedKeys: tree.getHalfCheckedKeys(), |
| | | checkedNodes: tree.getCheckedNodes(), |
| | | halfCheckedNodes: tree.getHalfCheckedNodes(), |
| | | totalChecked: tree.getCheckedKeys().length, |
| | | totalHalfChecked: tree.getHalfCheckedKeys().length |
| | | |
| | | watch( |
| | | () => props.visible, |
| | | async (isVisible) => { |
| | | if (isVisible) { |
| | | activeScopeType.value = props.scopeType || 'menu' |
| | | await ensureScopeLoaded(activeScopeType.value, { force: true }) |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.scopeType, |
| | | async (scopeType) => { |
| | | if (scopeType) { |
| | | activeScopeType.value = scopeType |
| | | if (props.visible) { |
| | | await ensureScopeLoaded(scopeType, { force: true }) |
| | | } |
| | | } |
| | | } |
| | | console.log('=== 选中的权限数据 ===', selectedData) |
| | | ElMessage.success(`已输出选中数据到控制台,共选中 ${selectedData.totalChecked} 个节点`) |
| | | } |
| | | ) |
| | | |
| | | watch( |
| | | activeScopeType, |
| | | async (scopeType) => { |
| | | if (props.visible && scopeType) { |
| | | await ensureScopeLoaded(scopeType) |
| | | } |
| | | } |
| | | ) |
| | | </script> |
| New file |
| | |
| | | <template> |
| | | <div class="art-full-height"> |
| | | <ArtSearchBar |
| | | v-model="searchForm" |
| | | :items="searchItems" |
| | | :showExpand="false" |
| | | @search="handleSearch" |
| | | @reset="handleReset" |
| | | /> |
| | | |
| | | <ElCard class="art-table-card"> |
| | | <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData" /> |
| | | |
| | | <ArtTable |
| | | :loading="loading" |
| | | :data="data" |
| | | :columns="columns" |
| | | :pagination="pagination" |
| | | @pagination:size-change="handleSizeChange" |
| | | @pagination:current-change="handleCurrentChange" |
| | | /> |
| | | </ElCard> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { fetchGetUserLoginList } from '@/api/system-manage' |
| | | import { useTable } from '@/hooks/core/useTable' |
| | | import { ElTag } from 'element-plus' |
| | | import { createUserLoginApiParams, userLoginPaginationKey } from './userLoginTable.config' |
| | | |
| | | defineOptions({ name: 'UserLogin' }) |
| | | |
| | | const LOGIN_TYPE_MAP = { |
| | | 0: { label: '登录成功', type: 'success' }, |
| | | 1: { label: '登录失败', type: 'danger' }, |
| | | 2: { label: '退出登录', type: 'info' }, |
| | | 3: { label: 'token 续签', type: 'warning' } |
| | | } |
| | | |
| | | const initialSearchForm = { |
| | | token: '', |
| | | ip: '', |
| | | type: void 0, |
| | | system: '' |
| | | } |
| | | |
| | | const searchForm = ref({ ...initialSearchForm }) |
| | | |
| | | const searchItems = computed(() => [ |
| | | { |
| | | label: 'Token', |
| | | key: 'token', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '请输入 token', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: 'IP', |
| | | key: 'ip', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '请输入 IP', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '系统', |
| | | key: 'system', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '请输入系统标识', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '类型', |
| | | key: 'type', |
| | | type: 'select', |
| | | props: { |
| | | placeholder: '请选择类型', |
| | | clearable: true, |
| | | options: Object.entries(LOGIN_TYPE_MAP).map(([value, item]) => ({ |
| | | label: item.label, |
| | | value: Number(value) |
| | | })) |
| | | } |
| | | } |
| | | ]) |
| | | |
| | | const getLoginTypeConfig = (type) => { |
| | | return ( |
| | | LOGIN_TYPE_MAP[type] || { |
| | | label: type ?? '-', |
| | | type: 'info' |
| | | } |
| | | ) |
| | | } |
| | | |
| | | const getUserDisplayName = (row) => { |
| | | return row.userId$ || row.userName || row.nickname || row.username || row.userId || '-' |
| | | } |
| | | |
| | | const { |
| | | columns, |
| | | columnChecks, |
| | | data, |
| | | loading, |
| | | pagination, |
| | | getData, |
| | | resetSearchParams, |
| | | replaceSearchParams, |
| | | handleSizeChange, |
| | | handleCurrentChange, |
| | | refreshData |
| | | } = useTable({ |
| | | core: { |
| | | apiFn: fetchGetUserLoginList, |
| | | apiParams: createUserLoginApiParams(searchForm.value), |
| | | paginationKey: userLoginPaginationKey, |
| | | columnsFactory: () => [ |
| | | { type: 'index', width: 60, label: '序号' }, |
| | | { prop: 'id', label: 'ID', minWidth: 90 }, |
| | | { |
| | | prop: 'userDisplayName', |
| | | label: '用户', |
| | | minWidth: 140, |
| | | formatter: (row) => getUserDisplayName(row) |
| | | }, |
| | | { prop: 'token', label: 'Token', minWidth: 260, showOverflowTooltip: true }, |
| | | { prop: 'ip', label: 'IP', minWidth: 140 }, |
| | | { |
| | | prop: 'type', |
| | | label: '类型', |
| | | width: 120, |
| | | formatter: (row) => { |
| | | const config = getLoginTypeConfig(row.type) |
| | | return h(ElTag, { type: config.type }, () => config.label) |
| | | } |
| | | }, |
| | | { prop: 'system', label: '系统', minWidth: 140 }, |
| | | { |
| | | prop: 'createTime', |
| | | label: '创建时间', |
| | | minWidth: 180, |
| | | sortable: true, |
| | | formatter: (row) => row.createTime$ || row.createTime || '-' |
| | | } |
| | | ] |
| | | } |
| | | }) |
| | | |
| | | const handleSearch = (params) => { |
| | | replaceSearchParams(params) |
| | | getData() |
| | | } |
| | | |
| | | const handleReset = () => { |
| | | Object.assign(searchForm.value, { ...initialSearchForm }) |
| | | resetSearchParams() |
| | | } |
| | | </script> |
| New file |
| | |
| | | function createUserLoginApiParams(filters = {}) { |
| | | return { |
| | | current: 1, |
| | | pageSize: 20, |
| | | ...filters |
| | | } |
| | | } |
| | | |
| | | const userLoginPaginationKey = { |
| | | current: 'current', |
| | | size: 'pageSize' |
| | | } |
| | | |
| | | export { createUserLoginApiParams, userLoginPaginationKey } |
| | |
| | | ref="searchBarRef" |
| | | v-model="formData" |
| | | :items="formItems" |
| | | :rules="rules" |
| | | @reset="handleReset" |
| | | @search="handleSearch" |
| | | > |
| | | </ArtSearchBar> |
| | | /> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { createUserSearchState } from '../userPage.helpers' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { required: true } |
| | | modelValue: { required: true }, |
| | | deptTreeOptions: { required: false, default: () => [] }, |
| | | roleOptions: { required: false, default: () => [] } |
| | | }) |
| | | |
| | | const emit = defineEmits(['update:modelValue', 'search', 'reset']) |
| | | const searchBarRef = ref() |
| | | |
| | | const formData = computed({ |
| | | get: () => props.modelValue, |
| | | set: (val) => emit('update:modelValue', val) |
| | | }) |
| | | const rules = { |
| | | // userName: [{ required: true, message: '请输入用户名', trigger: 'blur' }] |
| | | } |
| | | const statusOptions = ref([]) |
| | | function fetchStatusOptions() { |
| | | return new Promise((resolve) => { |
| | | setTimeout(() => { |
| | | resolve([ |
| | | { label: '在线', value: '1' }, |
| | | { label: '离线', value: '2' }, |
| | | { label: '异常', value: '3' }, |
| | | { label: '注销', value: '4' } |
| | | ]) |
| | | }, 1e3) |
| | | }) |
| | | } |
| | | onMounted(async () => { |
| | | statusOptions.value = await fetchStatusOptions() |
| | | }) |
| | | |
| | | const formItems = computed(() => [ |
| | | { |
| | | label: '用户名', |
| | | key: 'userName', |
| | | key: 'username', |
| | | type: 'input', |
| | | placeholder: '请输入用户名', |
| | | clearable: true |
| | | props: { |
| | | placeholder: '请输入用户名', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '昵称', |
| | | key: 'nickname', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '请输入昵称', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '手机号', |
| | | key: 'userPhone', |
| | | key: 'phone', |
| | | type: 'input', |
| | | props: { placeholder: '请输入手机号', maxlength: '11' } |
| | | props: { |
| | | placeholder: '请输入手机号', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '邮箱', |
| | | key: 'userEmail', |
| | | key: 'email', |
| | | type: 'input', |
| | | props: { placeholder: '请输入邮箱' } |
| | | props: { |
| | | placeholder: '请输入邮箱', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '工号', |
| | | key: 'code', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '请输入工号', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '部门', |
| | | key: 'deptId', |
| | | type: 'treeselect', |
| | | props: { |
| | | data: props.deptTreeOptions, |
| | | props: { |
| | | label: 'label', |
| | | value: 'value', |
| | | children: 'children' |
| | | }, |
| | | placeholder: '请选择部门', |
| | | clearable: true, |
| | | checkStrictly: true |
| | | } |
| | | }, |
| | | { |
| | | label: '角色', |
| | | key: 'roleIds', |
| | | type: 'select', |
| | | props: { |
| | | placeholder: '请选择角色', |
| | | clearable: true, |
| | | multiple: true, |
| | | collapseTags: true, |
| | | filterable: true, |
| | | options: props.roleOptions |
| | | } |
| | | }, |
| | | { |
| | | label: '性别', |
| | | key: 'sex', |
| | | type: 'select', |
| | | props: { |
| | | placeholder: '请选择性别', |
| | | clearable: true, |
| | | options: [ |
| | | { label: '未知', value: 0 }, |
| | | { label: '男', value: 1 }, |
| | | { label: '女', value: 2 } |
| | | ] |
| | | } |
| | | }, |
| | | { |
| | | label: '真实姓名', |
| | | key: 'realName', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '请输入真实姓名', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '身份证号', |
| | | key: 'idCard', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '请输入身份证号', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '关键字', |
| | | key: 'condition', |
| | | type: 'input', |
| | | props: { |
| | | placeholder: '输入关键字搜索', |
| | | clearable: true |
| | | } |
| | | }, |
| | | { |
| | | label: '状态', |
| | |
| | | type: 'select', |
| | | props: { |
| | | placeholder: '请选择状态', |
| | | options: statusOptions.value |
| | | } |
| | | }, |
| | | { |
| | | label: '性别', |
| | | key: 'userGender', |
| | | type: 'radiogroup', |
| | | props: { |
| | | clearable: true, |
| | | options: [ |
| | | { label: '男', value: '1' }, |
| | | { label: '女', value: '2' } |
| | | { label: '正常', value: 1 }, |
| | | { label: '禁用', value: 0 } |
| | | ] |
| | | } |
| | | } |
| | | ]) |
| | | |
| | | function handleReset() { |
| | | console.log('重置表单') |
| | | emit('update:modelValue', createUserSearchState()) |
| | | emit('reset') |
| | | } |
| | | |
| | | async function handleSearch(params) { |
| | | await searchBarRef.value.validate() |
| | | emit('search', params) |
| | | console.log('表单数据', params) |
| | | } |
| | | </script> |
| New file |
| | |
| | | export function createUserSearchState() { |
| | | return { |
| | | username: '', |
| | | nickname: '', |
| | | phone: '', |
| | | email: '', |
| | | status: void 0, |
| | | deptId: void 0, |
| | | roleIds: [], |
| | | code: '', |
| | | sex: void 0, |
| | | realName: '', |
| | | idCard: '', |
| | | condition: '' |
| | | } |
| | | } |
| | | |
| | | export function createUserFormState() { |
| | | return { |
| | | id: void 0, |
| | | username: '', |
| | | nickname: '', |
| | | deptId: 0, |
| | | roleIds: [], |
| | | userRoleIds: [], |
| | | password: '', |
| | | confirmPassword: '', |
| | | sex: 0, |
| | | code: '', |
| | | phone: '', |
| | | email: '', |
| | | realName: '', |
| | | idCard: '', |
| | | memo: '', |
| | | status: 1 |
| | | } |
| | | } |
| | | |
| | | export function buildUserSearchParams(params = {}) { |
| | | const searchParams = { |
| | | username: params.username, |
| | | nickname: params.nickname, |
| | | phone: params.phone, |
| | | email: params.email, |
| | | status: params.status, |
| | | deptId: params.deptId, |
| | | roleIds: normalizeRoleIds(params), |
| | | code: params.code, |
| | | sex: params.sex, |
| | | realName: params.realName, |
| | | idCard: params.idCard, |
| | | condition: params.condition |
| | | } |
| | | |
| | | return Object.fromEntries( |
| | | Object.entries(searchParams).filter(([, value]) => { |
| | | if (Array.isArray(value)) { |
| | | return value.length > 0 |
| | | } |
| | | return value !== '' && value !== undefined && value !== null |
| | | }) |
| | | ) |
| | | } |
| | | |
| | | export function buildUserPageQueryParams(params = {}) { |
| | | const { current, size, pageSize, ...filters } = params |
| | | return { |
| | | current: current || 1, |
| | | pageSize: pageSize || size || 20, |
| | | ...buildUserSearchParams(filters) |
| | | } |
| | | } |
| | | |
| | | export function buildUserDialogModel(record = {}) { |
| | | const roleIds = normalizeRoleIds(record) |
| | | return { |
| | | ...createUserFormState(), |
| | | id: record.id !== undefined && record.id !== null && record.id !== '' ? record.id : void 0, |
| | | username: record.username || '', |
| | | nickname: record.nickname || '', |
| | | deptId: normalizeDeptId(record.deptId), |
| | | roleIds, |
| | | userRoleIds: [...roleIds], |
| | | sex: record.sex !== undefined && record.sex !== null ? record.sex : 0, |
| | | code: record.code || '', |
| | | phone: record.phone || '', |
| | | email: record.email || '', |
| | | realName: record.realName || '', |
| | | idCard: record.idCard || '', |
| | | memo: record.memo || '', |
| | | status: record.status !== undefined && record.status !== null ? record.status : 1 |
| | | } |
| | | } |
| | | |
| | | export function mergeUserDetailRecord(detail = {}, fallback = {}) { |
| | | const merged = { |
| | | ...(fallback && typeof fallback === 'object' ? fallback : {}), |
| | | ...(detail && typeof detail === 'object' ? detail : {}) |
| | | } |
| | | |
| | | if (!hasRoleSelection(detail) && hasRoleSelection(fallback)) { |
| | | merged.roles = fallback.roles |
| | | } |
| | | |
| | | if (merged.deptLabel === undefined && fallback?.deptLabel) { |
| | | merged.deptLabel = fallback.deptLabel |
| | | } |
| | | |
| | | if (merged.roleNames === undefined && fallback?.roleNames) { |
| | | merged.roleNames = fallback.roleNames |
| | | } |
| | | |
| | | return merged |
| | | } |
| | | |
| | | export function buildUserSavePayload(form = {}) { |
| | | const roleIds = normalizeRoleIds(form) |
| | | const password = form.password || form.newPassword || '' |
| | | const payload = { |
| | | id: form.id !== undefined && form.id !== null && form.id !== '' ? form.id : void 0, |
| | | username: form.username || '', |
| | | nickname: form.nickname || '', |
| | | deptId: normalizeDeptId(form.deptId), |
| | | roleIds, |
| | | sex: form.sex !== undefined && form.sex !== null ? form.sex : 0, |
| | | code: form.code || '', |
| | | phone: form.phone || '', |
| | | email: form.email || '', |
| | | realName: form.realName || '', |
| | | idCard: form.idCard || '', |
| | | memo: form.memo || '', |
| | | status: form.status !== undefined && form.status !== null ? form.status : 1 |
| | | } |
| | | |
| | | if (password) { |
| | | payload.password = password |
| | | } |
| | | |
| | | return payload |
| | | } |
| | | |
| | | export function normalizeUserListRow(record = {}) { |
| | | const roles = Array.isArray(record.roles) ? record.roles : [] |
| | | const statusMeta = getUserStatusMeta(record.statusBool ?? record.status) |
| | | return { |
| | | ...record, |
| | | deptLabel: |
| | | record.deptLabel || |
| | | record.deptId$ || |
| | | record.deptName || |
| | | record.dept?.name || |
| | | (record.deptId === 0 ? '根部门' : '-'), |
| | | roleNames: formatUserRoleNames(roles) || record.roleNames || '', |
| | | statusBool: record.statusBool !== undefined ? Boolean(record.statusBool) : statusMeta.bool, |
| | | statusText: statusMeta.text, |
| | | statusType: statusMeta.type, |
| | | createTimeText: record.createTime$ || record.createTime || '', |
| | | updateTimeText: record.updateTime$ || record.updateTime || '' |
| | | } |
| | | } |
| | | |
| | | export function normalizeDeptTreeOptions(tree = []) { |
| | | if (!Array.isArray(tree)) { |
| | | return [] |
| | | } |
| | | |
| | | return tree |
| | | .map((node) => normalizeDeptTreeNode(node)) |
| | | .filter(Boolean) |
| | | } |
| | | |
| | | export function normalizeRoleOptions(roles = []) { |
| | | if (!Array.isArray(roles)) { |
| | | return [] |
| | | } |
| | | |
| | | return roles |
| | | .map((role) => { |
| | | if (!role || typeof role !== 'object') { |
| | | return null |
| | | } |
| | | const value = normalizeRoleId(role.id ?? role.roleId) |
| | | if (value === void 0) { |
| | | return null |
| | | } |
| | | return { |
| | | value, |
| | | label: role.name || role.roleName || role.code || '' |
| | | } |
| | | }) |
| | | .filter(Boolean) |
| | | } |
| | | |
| | | export function getUserStatusMeta(status) { |
| | | if (status === true || status === 1) { |
| | | return { type: 'success', text: '正常', bool: true } |
| | | } |
| | | if (status === false || status === 0) { |
| | | return { type: 'danger', text: '禁用', bool: false } |
| | | } |
| | | return { type: 'info', text: '未知', bool: false } |
| | | } |
| | | |
| | | export function formatUserRoleNames(roles = []) { |
| | | if (!Array.isArray(roles) || roles.length === 0) { |
| | | return '' |
| | | } |
| | | |
| | | const names = roles |
| | | .map((role) => { |
| | | if (typeof role === 'string') { |
| | | return role |
| | | } |
| | | if (role && typeof role === 'object') { |
| | | return role.name || role.roleName || role.code || '' |
| | | } |
| | | return '' |
| | | }) |
| | | .filter(Boolean) |
| | | |
| | | return names.join('、') |
| | | } |
| | | |
| | | function normalizeDeptTreeNode(node) { |
| | | if (!node || typeof node !== 'object') { |
| | | return null |
| | | } |
| | | |
| | | return { |
| | | value: normalizeDeptId(node.id ?? node.value), |
| | | label: node.name || node.label || '', |
| | | children: normalizeDeptTreeOptions(node.children) |
| | | } |
| | | } |
| | | |
| | | function normalizeRoleIds(source) { |
| | | if (!source || typeof source !== 'object') { |
| | | return [] |
| | | } |
| | | |
| | | const directRoleIds = Array.isArray(source.roleIds) |
| | | ? source.roleIds |
| | | : Array.isArray(source.userRoleIds) |
| | | ? source.userRoleIds |
| | | : Array.isArray(source.roles) |
| | | ? source.roles.map((item) => item?.id ?? item?.roleId) |
| | | : [] |
| | | |
| | | return Array.from( |
| | | new Set( |
| | | directRoleIds |
| | | .map((item) => normalizeRoleId(item)) |
| | | .filter((item) => item !== void 0) |
| | | ) |
| | | ) |
| | | } |
| | | |
| | | function normalizeDeptId(value) { |
| | | if (value === '' || value === null || value === undefined) { |
| | | return 0 |
| | | } |
| | | return value |
| | | } |
| | | |
| | | function normalizeRoleId(value) { |
| | | if (value === '' || value === null || value === undefined) { |
| | | return void 0 |
| | | } |
| | | const numeric = Number(value) |
| | | if (Number.isNaN(numeric)) { |
| | | return value |
| | | } |
| | | return numeric |
| | | } |
| | | |
| | | function hasRoleSelection(source) { |
| | | if (!source || typeof source !== 'object') { |
| | | return false |
| | | } |
| | | if (Array.isArray(source.roleIds) && source.roleIds.length > 0) { |
| | | return true |
| | | } |
| | | if (Array.isArray(source.userRoleIds) && source.userRoleIds.length > 0) { |
| | | return true |
| | | } |
| | | return Array.isArray(source.roles) && source.roles.length > 0 |
| | | } |
| New file |
| | |
| | | import assert from 'node:assert/strict' |
| | | import { readFile } from 'node:fs/promises' |
| | | import path from 'node:path' |
| | | import test from 'node:test' |
| | | |
| | | const projectRoot = path.resolve(import.meta.dirname, '..') |
| | | |
| | | async function readProjectFile(relativePath) { |
| | | return readFile(path.join(projectRoot, relativePath), 'utf8') |
| | | } |
| | | |
| | | test('development env routes API requests through the rsf-server context path', async () => { |
| | | const envContent = await readProjectFile('.env.development') |
| | | |
| | | assert.match(envContent, /VITE_API_URL\s*=\s*\/rsf-server\b/) |
| | | assert.match(envContent, /VITE_API_PROXY_URL\s*=\s*http:\/\/127\.0\.0\.1:8085\b/) |
| | | }) |
| | | |
| | | test('production env keeps the rsf-server context path as the API base', async () => { |
| | | const envContent = await readProjectFile('.env.production') |
| | | |
| | | assert.match(envContent, /VITE_API_URL\s*=\s*\/rsf-server\b/) |
| | | }) |
| | | |
| | | test('vite dev server proxies the rsf-server context path to the backend target', async () => { |
| | | const viteConfig = await readProjectFile('vite.config.js') |
| | | |
| | | assert.match(viteConfig, /'\/rsf-server'\s*:\s*\{/) |
| | | assert.match(viteConfig, /target:\s*VITE_API_PROXY_URL/) |
| | | }) |
| New file |
| | |
| | | import assert from 'node:assert/strict' |
| | | import fs from 'node:fs' |
| | | import test from 'node:test' |
| | | import { RoutesAlias } from '../src/router/routesAlias.js' |
| | | |
| | | test('adapts a backend menu tree using route for path and name for title', async () => { |
| | | const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js') |
| | | const result = adaptBackendMenuTree([ |
| | | { |
| | | id: 10, |
| | | name: 'menu.system', |
| | | parentId: 0, |
| | | path: '1', |
| | | route: '/system', |
| | | type: 0, |
| | | children: [ |
| | | { |
| | | id: 11, |
| | | name: 'menu.userLogin', |
| | | parentId: 10, |
| | | path: '1,10', |
| | | route: 'user-login', |
| | | component: 'userLogin', |
| | | type: 0 |
| | | } |
| | | ] |
| | | } |
| | | ]) |
| | | |
| | | assert.equal(result[0].path, 'system') |
| | | assert.equal(result[0].meta.title, '系统设置') |
| | | assert.equal(result[0].children[0].path, 'user-login') |
| | | assert.equal(result[0].children[0].meta.title, '登录日志') |
| | | assert.equal(result[0].children[0].component, '/system/user-login') |
| | | }) |
| | | |
| | | test('keeps backend leaves by deriving component paths from the route hierarchy when no alias exists', async () => { |
| | | const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js') |
| | | const result = adaptBackendMenuTree([ |
| | | { |
| | | id: 1, |
| | | name: 'menu.system', |
| | | parentId: 0, |
| | | path: '1', |
| | | route: '/system', |
| | | type: 0, |
| | | children: [ |
| | | { |
| | | id: 2, |
| | | name: 'menu.userLogin', |
| | | parentId: 1, |
| | | path: '1,2', |
| | | route: 'user-login', |
| | | component: 'userLogin', |
| | | type: 0 |
| | | }, |
| | | { |
| | | id: 3, |
| | | name: 'menu.host', |
| | | parentId: 1, |
| | | path: '1,3', |
| | | route: 'host', |
| | | component: 'host', |
| | | type: 0 |
| | | } |
| | | ] |
| | | } |
| | | ]) |
| | | |
| | | assert.equal(result[0].children.length, 2) |
| | | assert.equal(result[0].children[0].path, 'user-login') |
| | | assert.equal(result[0].children[0].meta.title, '登录日志') |
| | | assert.equal(result[0].children[0].component, '/system/user-login') |
| | | assert.equal(result[0].children[1].path, 'host') |
| | | assert.equal(result[0].children[1].meta.title, '机构管理') |
| | | assert.equal(result[0].children[1].component, '/system/host') |
| | | }) |
| | | |
| | | test('filters non-menu nodes while keeping plain string titles unchanged', async () => { |
| | | const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js') |
| | | const result = adaptBackendMenuTree([ |
| | | { |
| | | id: 20, |
| | | name: '系统设置', |
| | | parentId: 0, |
| | | path: '20', |
| | | route: '/system', |
| | | type: 0, |
| | | children: [ |
| | | { |
| | | id: 21, |
| | | name: 'menu.user', |
| | | parentId: 20, |
| | | path: '20,21', |
| | | route: 'user', |
| | | component: 'user', |
| | | type: 0 |
| | | }, |
| | | { |
| | | id: 22, |
| | | name: 'system:user:add', |
| | | parentId: 20, |
| | | path: '20,22', |
| | | route: 'user/add', |
| | | component: 'user', |
| | | type: 1 |
| | | } |
| | | ] |
| | | } |
| | | ]) |
| | | |
| | | assert.equal(result[0].meta.title, '系统设置') |
| | | assert.equal(result[0].children.length, 1) |
| | | assert.equal(result[0].children[0].id, '21') |
| | | assert.equal(result[0].children[0].component, '/system/user') |
| | | }) |
| | | |
| | | test('preserves absolute backend child routes instead of nesting them under the parent path', async () => { |
| | | const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js') |
| | | const result = adaptBackendMenuTree([ |
| | | { |
| | | id: 5318, |
| | | name: 'AI管理中心', |
| | | parentId: 0, |
| | | route: '/AI', |
| | | type: 0, |
| | | children: [ |
| | | { |
| | | id: 422, |
| | | name: 'menu.aiParam', |
| | | parentId: 5318, |
| | | route: '/system/aiParam', |
| | | component: 'aiParam', |
| | | type: 0 |
| | | } |
| | | ] |
| | | } |
| | | ]) |
| | | |
| | | assert.equal(result[0].path, 'AI') |
| | | assert.equal(result[0].children[0].path, '/system/aiParam') |
| | | assert.equal(result[0].children[0].meta.title, 'AI 参数') |
| | | assert.equal(result[0].children[0].component, '/system/aiParam') |
| | | }) |
| | | |
| | | test('keeps directory menus as layout nodes when they only group child routes', async () => { |
| | | const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js') |
| | | const result = adaptBackendMenuTree([ |
| | | { |
| | | id: 5318, |
| | | name: 'AI管理中心', |
| | | parentId: 0, |
| | | route: '/AI', |
| | | type: 0, |
| | | children: [ |
| | | { |
| | | id: 422, |
| | | name: 'menu.aiParam', |
| | | parentId: 5318, |
| | | route: '/system/aiParam', |
| | | component: 'aiParam', |
| | | type: 0 |
| | | } |
| | | ] |
| | | } |
| | | ]) |
| | | |
| | | assert.equal(result[0].component, RoutesAlias.Layout) |
| | | }) |
| | | |
| | | test('maps legacy backend icon names into renderable Iconify icons', async () => { |
| | | const { adaptBackendMenuTree } = await import('../src/router/adapters/backendMenuAdapter.js') |
| | | const result = adaptBackendMenuTree([ |
| | | { |
| | | id: 1, |
| | | name: 'AI管理中心', |
| | | route: '/ai', |
| | | icon: 'SmartToy', |
| | | type: 0, |
| | | children: [ |
| | | { |
| | | id: 2, |
| | | name: 'menu.userLogin', |
| | | route: 'user-login', |
| | | component: 'userLogin', |
| | | icon: 'Token', |
| | | type: 0 |
| | | } |
| | | ] |
| | | } |
| | | ]) |
| | | |
| | | assert.equal(result[0].meta.icon, 'ri:robot-2-line') |
| | | assert.equal(result[0].children[0].meta.icon, 'ri:key-2-line') |
| | | }) |
| | | |
| | | test('phase 1 resource mappings resolve to existing views and translatable menu labels', async () => { |
| | | const { PHASE_1_COMPONENTS } = await import('../src/router/adapters/backendMenuAdapter.js') |
| | | |
| | | Object.entries(PHASE_1_COMPONENTS).forEach(([resourceKey, viewPath]) => { |
| | | const normalizedPath = viewPath.replace(/^\//, '') |
| | | const componentWithIndex = new URL(`../src/views/${normalizedPath}/index.vue`, import.meta.url) |
| | | const singleFileComponent = new URL(`../src/views/${normalizedPath}.vue`, import.meta.url) |
| | | const componentExists = fs.existsSync(componentWithIndex) || fs.existsSync(singleFileComponent) |
| | | |
| | | assert.equal(componentExists, true, `${resourceKey} should resolve to an existing view`) |
| | | }) |
| | | }) |
| | |
| | | } |
| | | |
| | | function collectUsedIconsByPrefix() { |
| | | const iconPattern = /icon\s*[:=]\s*["']([a-z0-9-]+):([a-z0-9-]+)["']/g |
| | | const iconPattern = /["']([a-z0-9-]+):([a-z0-9-]+)["']/g |
| | | const knownPrefixes = new Set([ |
| | | 'fluent', |
| | | 'icon-park-outline', |
| | | 'iconamoon', |
| | | 'ix', |
| | | 'line-md', |
| | | 'ri', |
| | | 'svg-spinners', |
| | | 'system-uicons', |
| | | 'vaadin' |
| | | ]) |
| | | const usedIconsByPrefix = new Map() |
| | | |
| | | for (const filePath of collectSourceFiles(srcRoot)) { |
| | | const content = fs.readFileSync(filePath, 'utf8') |
| | | |
| | | for (const [, prefix, name] of content.matchAll(iconPattern)) { |
| | | if (!knownPrefixes.has(prefix)) { |
| | | continue |
| | | } |
| | | |
| | | const names = usedIconsByPrefix.get(prefix) || new Set() |
| | | names.add(name) |
| | | usedIconsByPrefix.set(prefix, names) |
| | |
| | | } |
| | | |
| | | function collectIconPrefixes() { |
| | | const iconPattern = /icon\s*[:=]\s*["']([a-z0-9-]+):/g |
| | | const iconPattern = /["']([a-z0-9-]+):([a-z0-9-]+)["']/g |
| | | const knownPrefixes = new Set([ |
| | | 'fluent', |
| | | 'icon-park-outline', |
| | | 'iconamoon', |
| | | 'ix', |
| | | 'line-md', |
| | | 'ri', |
| | | 'svg-spinners', |
| | | 'system-uicons', |
| | | 'vaadin' |
| | | ]) |
| | | const prefixes = new Set() |
| | | |
| | | for (const filePath of collectSourceFiles(srcRoot)) { |
| | | const content = fs.readFileSync(filePath, 'utf8') |
| | | |
| | | for (const match of content.matchAll(iconPattern)) { |
| | | prefixes.add(match[1]) |
| | | for (const [, prefix] of content.matchAll(iconPattern)) { |
| | | if (!knownPrefixes.has(prefix)) { |
| | | continue |
| | | } |
| | | |
| | | prefixes.add(prefix) |
| | | } |
| | | } |
| | | |
| New file |
| | |
| | | import assert from 'node:assert/strict' |
| | | import { readFileSync } from 'node:fs' |
| | | import test from 'node:test' |
| | | |
| | | import { |
| | | buildPreviewColumns, |
| | | buildReportStyleMeta |
| | | } from '../src/components/biz/list-export-print/list-export-print.helpers.js' |
| | | import { buildPrintDocumentHtml } from '../src/components/biz/list-export-print/list-print-document.js' |
| | | |
| | | const previewDialogSource = readFileSync( |
| | | new URL('../src/components/biz/list-export-print/list-print-preview-dialog.vue', import.meta.url), |
| | | 'utf8' |
| | | ) |
| | | const printDocumentSource = readFileSync( |
| | | new URL('../src/components/biz/list-export-print/list-print-document.js', import.meta.url), |
| | | 'utf8' |
| | | ) |
| | | |
| | | const scriptSetupIndex = previewDialogSource.indexOf('<script setup>') |
| | | const templateBlock = |
| | | scriptSetupIndex >= 0 |
| | | ? previewDialogSource.slice(previewDialogSource.indexOf('<template>'), scriptSetupIndex) |
| | | : previewDialogSource |
| | | |
| | | test('report style meta defaults to the shared print preview contract', () => { |
| | | assert.deepEqual(buildReportStyleMeta({ reportTitle: '角色报表' }).reportStyle, { |
| | | titleAlign: 'center', |
| | | titleLevel: 'strong', |
| | | orientation: 'portrait', |
| | | density: 'compact', |
| | | showSequence: true, |
| | | showBorder: true |
| | | }) |
| | | }) |
| | | |
| | | test('preview columns do not inject a sequence column when disabled', () => { |
| | | assert.deepEqual( |
| | | buildPreviewColumns({ |
| | | columns: [{ source: 'name', label: '角色名称' }], |
| | | reportStyle: { showSequence: false } |
| | | }), |
| | | [{ source: 'name', label: '角色名称' }] |
| | | ) |
| | | }) |
| | | |
| | | test('preview dialog template uses passed columns for sequence rendering and colspan', () => { |
| | | assert.equal(templateBlock.includes('>序号<'), false) |
| | | assert.match(templateBlock, /<div\s+:class="titleClass">\s*\{\{\s*meta\.reportTitle\s*\?\?\s*'--'\s*\}\}\s*<\/div>/) |
| | | assert.match(templateBlock, /<div\s+v-for="item in metaItems"[\s\S]*?:key="item\.key"[\s\S]*?>/) |
| | | assert.match(templateBlock, /\{\{\s*item\.label\s*\}\}/) |
| | | assert.match(templateBlock, /\{\{\s*item\.value\s*\?\?\s*'--'\s*\}\}/) |
| | | assert.match(templateBlock, /<th[\s\S]*?v-for="column in columns"[\s\S]*?>[\s\S]*?\{\{\s*column\.label\s*\}\}[\s\S]*?<\/th>/) |
| | | assert.match( |
| | | templateBlock, |
| | | /<td[\s\S]*?v-for="column in columns"[\s\S]*?>[\s\S]*?\{\{\s*column\.source === '__sequence__' \? index \+ 1 : row\?\.\[column\.source\] \?\? '--'\s*\}\}[\s\S]*?<\/td>/ |
| | | ) |
| | | assert.ok(templateBlock.includes("column.source === '__sequence__' ? index + 1")) |
| | | assert.ok(templateBlock.includes('Math.max(columns.length, 1)')) |
| | | assert.ok(previewDialogSource.includes('报表日期')) |
| | | assert.ok(previewDialogSource.includes('打印人')) |
| | | assert.ok(previewDialogSource.includes('打印时间')) |
| | | assert.ok(previewDialogSource.includes('记录数')) |
| | | assert.ok(templateBlock.includes('border-b')) |
| | | assert.ok(templateBlock.includes('print:hidden')) |
| | | }) |
| | | |
| | | test('preview dialog derives titleClass from reportStyle title alignment and level', () => { |
| | | assert.ok(previewDialogSource.includes('const titleClass = computed(() =>')) |
| | | assert.ok(previewDialogSource.includes('meta.reportTitle')) |
| | | assert.ok(previewDialogSource.includes('titleClass')) |
| | | assert.ok(previewDialogSource.includes('reportStyle.titleAlign')) |
| | | assert.ok(previewDialogSource.includes('reportStyle.titleLevel')) |
| | | assert.ok(previewDialogSource.includes("left: 'text-left'")) |
| | | assert.ok(previewDialogSource.includes("center: 'text-center'")) |
| | | assert.ok(previewDialogSource.includes("right: 'text-right'")) |
| | | assert.ok(previewDialogSource.includes("normal: 'text-[18px] font-medium'")) |
| | | assert.ok(previewDialogSource.includes("strong: 'text-[22px] font-semibold'")) |
| | | assert.ok(previewDialogSource.includes("prominent: 'text-[26px] font-bold'")) |
| | | assert.ok(previewDialogSource.includes("?? alignMap.center")) |
| | | assert.ok(previewDialogSource.includes("?? levelMap.strong")) |
| | | }) |
| | | |
| | | test('preview dialog keeps report content compact and prioritizes showing all columns', () => { |
| | | assert.ok(previewDialogSource.includes("text-[18px] font-medium")) |
| | | assert.ok(previewDialogSource.includes("text-[22px] font-semibold")) |
| | | assert.ok(previewDialogSource.includes("text-[26px] font-bold")) |
| | | assert.ok(previewDialogSource.includes('grid grid-cols-4 gap-2 text-[11px]')) |
| | | assert.ok(previewDialogSource.includes('table-auto')) |
| | | assert.ok(previewDialogSource.includes('text-[11px] leading-tight')) |
| | | assert.ok(previewDialogSource.includes('px-1.5 py-1.5')) |
| | | assert.ok(previewDialogSource.includes('break-all')) |
| | | }) |
| | | |
| | | test('preview dialog prints through a standalone report document instead of window.print on the app shell', () => { |
| | | assert.equal(previewDialogSource.includes('window.print()'), false) |
| | | assert.ok(previewDialogSource.includes("import { printReportDocument } from './list-print-document.js'")) |
| | | assert.ok(previewDialogSource.includes('printReportDocument({')) |
| | | }) |
| | | |
| | | test('standalone print document preserves report title, meta row, sequence column, and print page styles', () => { |
| | | const html = buildPrintDocumentHtml({ |
| | | title: '角色管理报表', |
| | | meta: { |
| | | reportTitle: '角色管理报表', |
| | | reportDate: '2026/3/29', |
| | | operator: 'root', |
| | | printedAt: '2026/3/29 17:31:00', |
| | | count: 2, |
| | | reportStyle: { |
| | | titleAlign: 'center', |
| | | titleLevel: 'strong', |
| | | orientation: 'portrait' |
| | | } |
| | | }, |
| | | columns: [ |
| | | { source: '__sequence__', label: '序号', align: 'center' }, |
| | | { source: 'name', label: '角色名称' }, |
| | | { source: 'code', label: '角色编码' } |
| | | ], |
| | | rows: [ |
| | | { id: 1, name: 'WMS系统管理员', code: 'admin' }, |
| | | { id: 2, name: 'ERP财务管理员', code: 'erpcw' } |
| | | ] |
| | | }) |
| | | |
| | | assert.ok(html.includes('<title>角色管理报表</title>')) |
| | | assert.ok(html.includes('报表日期')) |
| | | assert.ok(html.includes('打印人')) |
| | | assert.ok(html.includes('打印时间')) |
| | | assert.ok(html.includes('记录数')) |
| | | assert.ok(html.includes('序号')) |
| | | assert.ok(html.includes('WMS系统管理员')) |
| | | assert.ok(html.includes('ERP财务管理员')) |
| | | assert.ok(html.includes('@page')) |
| | | assert.ok(html.includes('size: A4 portrait;')) |
| | | const pageRule = html.match(/@page\s*\{[\s\S]*?\}/)?.[0] ?? '' |
| | | assert.equal(pageRule.includes('margin:'), false) |
| | | assert.ok(html.includes('print-color-adjust: exact;')) |
| | | assert.ok(html.includes('window.print()')) |
| | | assert.ok(html.includes('window.close()')) |
| | | assert.ok(html.includes('font-size: 11px;')) |
| | | assert.ok(html.includes('font-size: 22px;')) |
| | | assert.ok(html.includes('table-layout: auto;')) |
| | | }) |
| | | |
| | | test('standalone print document opens a writable popup window for document injection', () => { |
| | | assert.equal(printDocumentSource.includes('noopener,noreferrer'), false) |
| | | assert.ok(printDocumentSource.includes("window.open('', '_blank')")) |
| | | assert.ok(printDocumentSource.includes('printWindow.document.write(buildPrintDocumentHtml(payload))')) |
| | | }) |
| | | |
| | | test('preview dialog caps rendered rows and keeps the table body scrollable for large datasets', () => { |
| | | assert.ok(previewDialogSource.includes("maxPreviewRows: { type: Number, default: 50 }")) |
| | | assert.ok(previewDialogSource.includes('const previewRows = computed(() =>')) |
| | | assert.ok(previewDialogSource.includes('props.rows.slice(0, props.maxPreviewRows)')) |
| | | assert.ok(previewDialogSource.includes('const hiddenRowCount = computed(() =>')) |
| | | assert.ok(previewDialogSource.includes('预览仅展示前')) |
| | | assert.ok(previewDialogSource.includes('min-h-0 flex-1 overflow-auto')) |
| | | assert.ok(previewDialogSource.includes('v-for="(row, index) in previewRows"')) |
| | | assert.ok(previewDialogSource.includes('v-if="hiddenRowCount > 0"')) |
| | | }) |
| | | |
| | | test('preview dialog keeps the whole modal inside the viewport and lets only the table area scroll', () => { |
| | | assert.ok(previewDialogSource.includes('width="min(96vw, 1100px)"')) |
| | | assert.ok(previewDialogSource.includes('top="4vh"')) |
| | | assert.ok(previewDialogSource.includes('class="max-h-[88vh] overflow-hidden"')) |
| | | assert.ok(previewDialogSource.includes('flex max-h-[calc(88vh-160px)] min-h-0 flex-col')) |
| | | assert.ok(previewDialogSource.includes('mx-auto flex min-h-0 flex-1 flex-col overflow-hidden')) |
| | | assert.ok(previewDialogSource.includes(':class="paperClass"')) |
| | | assert.ok(previewDialogSource.includes(':style="paperStyle"')) |
| | | assert.ok(previewDialogSource.includes('mt-4 min-h-0 flex-1 overflow-auto')) |
| | | }) |
| | | |
| | | test('preview dialog lets the user switch between portrait and landscape before printing', () => { |
| | | assert.ok(previewDialogSource.includes("const currentOrientation = ref('portrait')")) |
| | | assert.ok(previewDialogSource.includes('watch(')) |
| | | assert.ok(previewDialogSource.includes('v-model="currentOrientation"')) |
| | | assert.ok(previewDialogSource.includes('label="portrait"')) |
| | | assert.ok(previewDialogSource.includes('label="landscape"')) |
| | | assert.ok(previewDialogSource.includes('竖版')) |
| | | assert.ok(previewDialogSource.includes('横版')) |
| | | assert.ok(previewDialogSource.includes('const effectiveMeta = computed(() =>')) |
| | | assert.ok(previewDialogSource.includes('orientation: currentOrientation.value')) |
| | | assert.ok(previewDialogSource.includes('meta: effectiveMeta.value')) |
| | | assert.ok(previewDialogSource.includes('const paperClass = computed(() =>')) |
| | | assert.ok(previewDialogSource.includes("currentOrientation.value === 'landscape'")) |
| | | assert.ok(previewDialogSource.includes("aspectRatio: currentOrientation.value === 'landscape' ? '297 / 210' : '210 / 297'")) |
| | | }) |
| | | |
| | | test('preview dialog lets the user toggle table borders and carries the setting into print meta', () => { |
| | | assert.ok(previewDialogSource.includes("const currentShowBorder = ref(true)")) |
| | | assert.ok(previewDialogSource.includes('v-model="currentShowBorder"')) |
| | | assert.ok(previewDialogSource.includes('边框开')) |
| | | assert.ok(previewDialogSource.includes('边框关')) |
| | | assert.ok(previewDialogSource.includes('showBorder: currentShowBorder.value')) |
| | | assert.ok(previewDialogSource.includes('const tableWrapClass = computed(() =>')) |
| | | assert.ok(previewDialogSource.includes('const headerCellClass = computed(() =>')) |
| | | assert.ok(previewDialogSource.includes('const bodyCellClass = computed(() =>')) |
| | | assert.ok(printDocumentSource.includes('const showBorder = reportStyle.showBorder !== false')) |
| | | assert.ok(printDocumentSource.includes("const tableWrapClass = showBorder ? 'report-table-wrap' : 'report-table-wrap report-table-wrap-borderless'")) |
| | | }) |
| New file |
| | |
| | | import test from 'node:test' |
| | | import assert from 'node:assert/strict' |
| | | |
| | | test('getFirstMenuPath skips unreleased menu routes and selects the first implemented page', async () => { |
| | | const { getFirstMenuPath } = await import('../src/utils/navigation/route.js') |
| | | |
| | | const menuList = [ |
| | | { |
| | | path: '/system', |
| | | children: [ |
| | | { |
| | | path: '/system/aiParam', |
| | | component: '/system/aiParam', |
| | | meta: { title: 'AI 参数' } |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | path: '/logs', |
| | | children: [ |
| | | { |
| | | path: '/logs/system/userLogin', |
| | | component: '/system/user-login', |
| | | meta: { title: '登录日志' } |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | |
| | | assert.equal(getFirstMenuPath(menuList), '/logs/system/userLogin') |
| | | }) |
| | | |
| | | test('getFirstMenuPath keeps traversing siblings when the first branch has no implemented leaf', async () => { |
| | | const { getFirstMenuPath } = await import('../src/utils/navigation/route.js') |
| | | |
| | | const menuList = [ |
| | | { |
| | | path: '/ai', |
| | | children: [ |
| | | { |
| | | path: '/ai/param', |
| | | component: '/system/aiParam', |
| | | meta: { title: 'AI 参数' } |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | path: '/permissions', |
| | | children: [ |
| | | { |
| | | path: '/permissions/system', |
| | | children: [ |
| | | { |
| | | path: '/permissions/system/role', |
| | | component: '/system/role', |
| | | meta: { title: '角色管理' } |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | |
| | | assert.equal(getFirstMenuPath(menuList), '/permissions/system/role') |
| | | }) |
| | |
| | | import assert from 'node:assert/strict' |
| | | import { register } from 'node:module' |
| | | import test from 'node:test' |
| | | import { buildUserListParams, buildRoleListParams } from '../src/api/system-manage.js' |
| | | |
| | | const menuTreeFixture = [ |
| | | { |
| | | id: 1, |
| | | parentId: 0, |
| | | name: 'menu.user', |
| | | route: 'user', |
| | | type: 0, |
| | | component: 'user', |
| | | authority: 'system:user', |
| | | icon: 'People', |
| | | sort: 1, |
| | | status: 1, |
| | | memo: 'User menu', |
| | | children: [ |
| | | { |
| | | id: 2, |
| | | parentId: 1, |
| | | name: 'Query User', |
| | | type: 1, |
| | | authority: 'system:user:list', |
| | | icon: 'Search', |
| | | sort: 1, |
| | | status: 0, |
| | | memo: 'Query action' |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | |
| | | register( |
| | | `data:text/javascript,export function resolve(specifier, context, nextResolve){ if(specifier===\'@/utils/http\'){ return { shortCircuit:true, url:\'data:text/javascript,export default { get(){ return Promise.resolve({}) }, post(config){ if(config?.url===\\'/menu/list\\'){ globalThis.__lastMenuListConfig = config; return Promise.resolve(${JSON.stringify(menuTreeFixture.flatMap((node) => [node, ...node.children]))}) } if(config?.url===\\'/menu/tree\\'){ globalThis.__lastMenuTreeConfig = config; return Promise.resolve(${JSON.stringify(menuTreeFixture)}) } return Promise.resolve(config) } }\' } } return nextResolve(specifier, context) }`, |
| | | import.meta.url |
| | | ) |
| | | |
| | | const { |
| | | buildUserListParams, |
| | | buildRoleListParams, |
| | | fetchDeleteMenu, |
| | | fetchGetMenuList, |
| | | fetchGetMenuTree, |
| | | fetchResetUserPassword, |
| | | fetchGetRoleScopeList, |
| | | fetchGetRoleScopeTree, |
| | | fetchSaveMenu, |
| | | fetchUpdateMenu |
| | | } = await import('../src/api/system-manage.js') |
| | | |
| | | test('buildUserListParams matches the rsf-admin paging contract', () => { |
| | | assert.equal(typeof buildUserListParams, 'function') |
| | | assert.equal(typeof buildRoleListParams, 'function') |
| | | |
| | | assert.deepEqual( |
| | | buildUserListParams({ |
| | | current: 2, |
| | | pageSize: 20, |
| | | username: 'root', |
| | | email: 'root@example.com', |
| | | deptId: 3 |
| | | }), |
| | | { |
| | | current: 2, |
| | | pageSize: 20, |
| | | username: 'root', |
| | | email: 'root@example.com', |
| | | deptId: 3 |
| | | } |
| | | ) |
| | | }) |
| | | |
| | | test('fetchResetUserPassword requires an admin update payload', () => { |
| | | assert.throws(() => fetchResetUserPassword(1), /object payload/i) |
| | | assert.throws(() => fetchResetUserPassword({ id: 1 }), /password/i) |
| | | |
| | | return fetchResetUserPassword({ id: 1, newPassword: 'secret' }).then((config) => { |
| | | assert.equal(config.params.password, 'secret') |
| | | }) |
| | | }) |
| | | |
| | | test('fetchGetMenuList folds button nodes into authList', async () => { |
| | | const menuList = await fetchGetMenuList() |
| | | |
| | | assert.equal(menuList.length, 1) |
| | | assert.equal(menuList[0].id, '1') |
| | | assert.equal(menuList[0].parentId, '0') |
| | | assert.equal(menuList[0].route, 'user') |
| | | assert.equal(menuList[0].authority, 'system:user') |
| | | assert.equal(menuList[0].status, 1) |
| | | assert.equal(menuList[0].memo, 'User menu') |
| | | assert.equal(menuList[0].meta.title, 'menus.user') |
| | | assert.equal(menuList[0].component, 'user') |
| | | assert.equal(menuList[0].meta.sort, 1) |
| | | assert.equal(menuList[0].meta.isEnable, true) |
| | | assert.deepEqual(menuList[0].meta.authList, [ |
| | | { |
| | | id: '2', |
| | | parentId: '1', |
| | | parentName: '', |
| | | name: 'Query User', |
| | | title: 'Query User', |
| | | route: '', |
| | | component: '', |
| | | authMark: 'system:user:list', |
| | | authority: 'system:user:list', |
| | | icon: 'Search', |
| | | sort: 1, |
| | | status: 0, |
| | | memo: 'Query action', |
| | | type: 1 |
| | | } |
| | | ]) |
| | | assert.equal(menuList[0].children.length, 0) |
| | | assert.deepEqual(globalThis.__lastMenuListConfig?.params, {}) |
| | | }) |
| | | |
| | | test('menu tree helpers always send an explicit empty body for Spring Map request bodies', async () => { |
| | | await fetchGetMenuTree() |
| | | assert.deepEqual(globalThis.__lastMenuTreeConfig?.params, {}) |
| | | |
| | | await fetchGetRoleScopeTree('menu') |
| | | assert.equal(globalThis.__lastMenuTreeConfig?.url, '/menu/tree') |
| | | assert.deepEqual(globalThis.__lastMenuTreeConfig?.params, {}) |
| | | }) |
| | | |
| | | test('menu CRUD helpers target the real rsf-server endpoints', async () => { |
| | | assert.equal(typeof fetchSaveMenu, 'function') |
| | | assert.equal(typeof fetchUpdateMenu, 'function') |
| | | assert.equal(typeof fetchDeleteMenu, 'function') |
| | | |
| | | const saveConfig = await fetchSaveMenu({ name: '菜单' }) |
| | | const updateConfig = await fetchUpdateMenu({ id: 1, name: '菜单' }) |
| | | const deleteConfig = await fetchDeleteMenu('1,2') |
| | | |
| | | assert.equal(saveConfig.url, '/menu/save') |
| | | assert.deepEqual(saveConfig.params, { name: '菜单' }) |
| | | assert.equal(updateConfig.url, '/menu/update') |
| | | assert.deepEqual(updateConfig.params, { id: 1, name: '菜单' }) |
| | | assert.equal(deleteConfig.url, '/menu/remove/1,2') |
| | | }) |
| | | |
| | | test('scope resolvers fail fast on invalid scope types', () => { |
| | | assert.throws(() => fetchGetRoleScopeList('invalid', 1), /Unsupported scope type/i) |
| | | assert.throws(() => fetchGetRoleScopeTree('invalid', {}), /Unsupported scope type/i) |
| | | }) |
| | |
| | | import assert from 'node:assert/strict' |
| | | import fs from 'node:fs' |
| | | import path from 'node:path' |
| | | import { readFileSync } from 'node:fs' |
| | | import test from 'node:test' |
| | | |
| | | const projectRoot = path.resolve(import.meta.dirname, '..') |
| | | const zhLocalePath = path.join(projectRoot, 'src', 'locales', 'langs', 'zh.json') |
| | | const enLocalePath = path.join(projectRoot, 'src', 'locales', 'langs', 'en.json') |
| | | const routerUtilsPath = path.join(projectRoot, 'src', 'utils', 'router.js') |
| | | const menuPagePath = path.join(projectRoot, 'src', 'views', 'system', 'menu', 'index.vue') |
| | | const menuPageSource = readFileSync( |
| | | new URL('../src/views/system/menu/index.vue', import.meta.url), |
| | | 'utf8' |
| | | ) |
| | | |
| | | test('current-system menu keys are available in rsf-design locales', () => { |
| | | const zhMessages = JSON.parse(fs.readFileSync(zhLocalePath, 'utf8')) |
| | | const enMessages = JSON.parse(fs.readFileSync(enLocalePath, 'utf8')) |
| | | const menuDialogSource = readFileSync( |
| | | new URL('../src/views/system/menu/modules/menu-dialog.vue', import.meta.url), |
| | | 'utf8' |
| | | ) |
| | | |
| | | assert.equal(zhMessages.menu?.system, '系统设置') |
| | | assert.equal(zhMessages.menu?.basicInfo, '基础信息') |
| | | assert.equal(zhMessages.menu?.aiParam, 'AI 参数') |
| | | const routerSource = readFileSync(new URL('../src/utils/router.js', import.meta.url), 'utf8') |
| | | const zhLocale = JSON.parse(readFileSync(new URL('../src/locales/langs/zh.json', import.meta.url), 'utf8')) |
| | | const enLocale = JSON.parse(readFileSync(new URL('../src/locales/langs/en.json', import.meta.url), 'utf8')) |
| | | |
| | | assert.equal(enMessages.menu?.system, 'System') |
| | | assert.equal(enMessages.menu?.basicInfo, 'BasicInfo') |
| | | assert.equal(enMessages.menu?.aiParam, 'AI Params') |
| | | test('menu page submit and delete handlers call the real backend menu APIs', () => { |
| | | assert.match(menuPageSource, /fetchSaveMenu/) |
| | | assert.match(menuPageSource, /fetchUpdateMenu/) |
| | | assert.match(menuPageSource, /fetchDeleteMenu/) |
| | | assert.doesNotMatch(menuPageSource, /console\.log\('提交数据:'/) |
| | | assert.match(menuPageSource, /const handleAddAuth = \(row\) =>/) |
| | | assert.match(menuPageSource, /const handleDeleteMenu = async \(row\) =>/) |
| | | assert.match(menuPageSource, /const handleDeleteAuth = async \(row\) =>/) |
| | | assert.match(menuPageSource, /await fetchDeleteMenu\(row\.id\)/) |
| | | }) |
| | | |
| | | test('formatMenuTitle recognizes current-system menu translation keys', () => { |
| | | const routerSource = fs.readFileSync(routerUtilsPath, 'utf8') |
| | | |
| | | assert.match(routerSource, /startsWith\('menu\.'\)|startsWith\("menu\."\)/) |
| | | test('menu dialog accepts edit data and parent menu options from the page', () => { |
| | | assert.match(menuDialogSource, /editData:/) |
| | | assert.match(menuDialogSource, /menuTreeOptions:/) |
| | | assert.match(menuDialogSource, /key: 'parentId'/) |
| | | assert.match(menuDialogSource, /type: 'treeselect'/) |
| | | }) |
| | | |
| | | test('menu management table shows icon preview and translated names', () => { |
| | | const menuPageSource = fs.readFileSync(menuPagePath, 'utf8') |
| | | |
| | | test('menu page renders Iconify preview and current-system translated menu names', () => { |
| | | assert.ok( |
| | | menuPageSource.indexOf("label: '菜单名称'") < menuPageSource.indexOf("label: '图标预览'"), |
| | | '菜单名称列应保持在图标预览列之前,避免树形展开箭头跑到图标列' |
| | | ) |
| | | assert.match(menuPageSource, /label:\s*'图标预览'/) |
| | | assert.match(menuPageSource, /ArtSvgIcon/) |
| | | assert.match(menuPageSource, /row\.meta\?\.title\s*\|\|\s*row\.name|item\.meta\?\.title\s*\|\|\s*item\.name/) |
| | | assert.match(menuPageSource, /rounded-md/) |
| | | assert.match(menuPageSource, /row\.meta\?\.title\s*\|\|\s*row\.name/) |
| | | assert.match(routerSource, /startsWith\('menu\.'\)|startsWith\("menu\."\)/) |
| | | assert.match(routerSource, /const fallbackTitle =/) |
| | | assert.match(routerSource, /title\.startsWith\('menus\.'\)/) |
| | | assert.match(routerSource, /i18n\.global\.te\(fallbackTitle\)/) |
| | | assert.equal(zhLocale.menu?.system, '系统设置') |
| | | assert.equal(zhLocale.menu?.basicInfo, '基础信息') |
| | | assert.equal(zhLocale.menu?.aiParam, 'AI 参数') |
| | | assert.equal(enLocale.menu?.system, 'System') |
| | | assert.equal(enLocale.menu?.basicInfo, 'BasicInfo') |
| | | assert.equal(enLocale.menu?.aiParam, 'AI Params') |
| | | }) |
| | |
| | | import assert from 'node:assert/strict' |
| | | import fs from 'node:fs' |
| | | import test from 'node:test' |
| | | import { |
| | | buildScopeTreeNodes, |
| | | buildScopeSavePayload, |
| | | SCOPE_TYPES |
| | | } from '../src/views/system/role/roleScope.config.js' |
| | | |
| | | test('menu scope nodes preserve backend ids for save', () => { |
| | | const tree = buildScopeTreeNodes(SCOPE_TYPES.menu, [{ id: 1, label: '系统管理' }]) |
| | | assert.equal(tree[0].id, 1) |
| | | import { |
| | | buildRoleDialogModel, |
| | | buildRolePageQueryParams, |
| | | buildRoleSavePayload, |
| | | buildRoleScopeSubmitPayload, |
| | | buildRoleSearchParams, |
| | | getRoleScopeConfig, |
| | | normalizeRoleScopeTreeData, |
| | | normalizeRoleListRow, |
| | | createRoleSearchState |
| | | } from '../src/views/system/role/rolePage.helpers.js' |
| | | import { resolveBackendMenuTitle } from '../src/utils/backend-menu-title.js' |
| | | |
| | | const rolePermissionDialogSource = fs.readFileSync( |
| | | new URL('../src/views/system/role/modules/role-permission-dialog.vue', import.meta.url), |
| | | 'utf8' |
| | | ) |
| | | const roleEditDialogSource = fs.readFileSync( |
| | | new URL('../src/views/system/role/modules/role-edit-dialog.vue', import.meta.url), |
| | | 'utf8' |
| | | ) |
| | | const roleIndexSource = fs.readFileSync( |
| | | new URL('../src/views/system/role/index.vue', import.meta.url), |
| | | 'utf8' |
| | | ) |
| | | |
| | | test('buildRoleSearchParams keeps real role search fields', () => { |
| | | assert.deepEqual( |
| | | buildRoleSearchParams({ |
| | | name: '管理员', |
| | | code: 'R_ADMIN', |
| | | memo: '核心角色', |
| | | status: 1, |
| | | condition: 'admin' |
| | | }), |
| | | { |
| | | name: '管理员', |
| | | code: 'R_ADMIN', |
| | | memo: '核心角色', |
| | | status: 1, |
| | | condition: 'admin' |
| | | } |
| | | ) |
| | | }) |
| | | |
| | | test('scope save payload is delegated per scope type', () => { |
| | | const payload = buildScopeSavePayload(SCOPE_TYPES.menu, { |
| | | roleId: 9, |
| | | selectedIds: [1, 2], |
| | | authType: 0 |
| | | test('buildRolePageQueryParams merges paging and search fields', () => { |
| | | assert.deepEqual( |
| | | buildRolePageQueryParams({ |
| | | current: 3, |
| | | size: 20, |
| | | name: '管理员' |
| | | }), |
| | | { |
| | | current: 3, |
| | | pageSize: 20, |
| | | name: '管理员' |
| | | } |
| | | ) |
| | | }) |
| | | |
| | | test('buildRoleDialogModel normalizes backend role data into the form model', () => { |
| | | assert.deepEqual( |
| | | buildRoleDialogModel({ |
| | | id: 7, |
| | | name: '管理员', |
| | | code: 'R_ADMIN', |
| | | memo: '核心角色', |
| | | status: 0 |
| | | }), |
| | | { |
| | | id: 7, |
| | | name: '管理员', |
| | | code: 'R_ADMIN', |
| | | memo: '核心角色', |
| | | status: 0 |
| | | } |
| | | ) |
| | | }) |
| | | |
| | | test('buildRoleSavePayload submits backend role fields', () => { |
| | | assert.deepEqual( |
| | | buildRoleSavePayload({ |
| | | id: 7, |
| | | name: '管理员', |
| | | code: 'R_ADMIN', |
| | | memo: '核心角色', |
| | | status: 1 |
| | | }), |
| | | { |
| | | id: 7, |
| | | name: '管理员', |
| | | code: 'R_ADMIN', |
| | | memo: '核心角色', |
| | | status: 1 |
| | | } |
| | | ) |
| | | }) |
| | | |
| | | test('normalizeRoleListRow exposes table friendly fields', () => { |
| | | const normalized = normalizeRoleListRow({ |
| | | id: 7, |
| | | name: '管理员', |
| | | code: 'R_ADMIN', |
| | | memo: '核心角色', |
| | | status: 1, |
| | | createTime: '2025-03-28 10:00:00', |
| | | updateTime: '2025-03-28 11:00:00' |
| | | }) |
| | | assert.equal(payload.id, 9) |
| | | |
| | | assert.equal(normalized.name, '管理员') |
| | | assert.equal(normalized.statusBool, true) |
| | | assert.equal(normalized.statusText, '正常') |
| | | assert.equal(normalized.statusType, 'success') |
| | | assert.equal(normalized.createTimeText, '2025-03-28 10:00:00') |
| | | assert.equal(normalized.updateTimeText, '2025-03-28 11:00:00') |
| | | }) |
| | | |
| | | test('buildRoleScopeSubmitPayload normalizes checked keys for backend scope update', () => { |
| | | assert.deepEqual( |
| | | buildRoleScopeSubmitPayload(7, ['1', 2], ['3']), |
| | | { |
| | | id: 7, |
| | | menuIds: { |
| | | checked: [1, 2], |
| | | halfChecked: [3] |
| | | } |
| | | } |
| | | ) |
| | | }) |
| | | |
| | | test('getRoleScopeConfig resolves the four backend scope contracts', () => { |
| | | assert.deepEqual(getRoleScopeConfig('menu'), { |
| | | scopeType: 'menu', |
| | | title: '网页权限', |
| | | listUrl: '/role/scope/list', |
| | | treeUrl: '/menu/tree' |
| | | }) |
| | | assert.deepEqual(getRoleScopeConfig('warehouse'), { |
| | | scopeType: 'warehouse', |
| | | title: '仓库权限', |
| | | listUrl: '/roleWarehouse/scope/list', |
| | | treeUrl: '/menuWarehouse/tree' |
| | | }) |
| | | assert.throws(() => getRoleScopeConfig('invalid'), /Unsupported scope type/i) |
| | | }) |
| | | |
| | | test('normalizeRoleScopeTreeData folds menu auth buttons into child nodes', () => { |
| | | const tree = normalizeRoleScopeTreeData('menu', [ |
| | | { |
| | | id: 1, |
| | | name: 'menu.role', |
| | | type: 0, |
| | | children: [ |
| | | { |
| | | id: 2, |
| | | name: 'Query Role', |
| | | type: 1, |
| | | authority: 'system:role:list' |
| | | } |
| | | ] |
| | | } |
| | | ]) |
| | | |
| | | assert.equal(tree[0].label, 'menus.role') |
| | | assert.equal(tree[0].children[0].isAuthButton, true) |
| | | assert.equal(tree[0].children[0].authMark, 'system:role:list') |
| | | }) |
| | | |
| | | test('resolveBackendMenuTitle translates legacy backend menu keys into readable labels', () => { |
| | | assert.equal(resolveBackendMenuTitle('menu.role'), '角色管理') |
| | | assert.equal(resolveBackendMenuTitle('menus.aiParam'), 'AI 参数') |
| | | assert.equal(resolveBackendMenuTitle('AI管理中心'), 'AI管理中心') |
| | | }) |
| | | |
| | | test('role permission dialog only loads the active scope instead of all scopes together', () => { |
| | | assert.match(rolePermissionDialogSource, /ensureScopeLoaded\(activeScopeType\.value, \{ force: true \}\)/) |
| | | assert.doesNotMatch(rolePermissionDialogSource, /loadAllScopeData/) |
| | | }) |
| | | |
| | | test('role permission dialog keeps scope tree search and readable labels', () => { |
| | | assert.match(rolePermissionDialogSource, /placeholder="搜索权限树"/) |
| | | assert.match(rolePermissionDialogSource, /reloadSelection: false/) |
| | | assert.match(rolePermissionDialogSource, /resolveBackendMenuTitle/) |
| | | }) |
| | | |
| | | test('role edit dialog keeps code optional to match the backend contract', () => { |
| | | assert.match(roleEditDialogSource, /name: \[\{ required: true, message: '请输入角色名称'/) |
| | | assert.doesNotMatch(roleEditDialogSource, /code: \[\{ required: true, message: '请输入角色编码'/) |
| | | }) |
| | | |
| | | test('role page uses semantic auth aliases so backend permissions render actions', () => { |
| | | assert.match(roleIndexSource, /v-auth=\"'add'\"/) |
| | | assert.match(roleIndexSource, /v-auth=\"'delete'\"/) |
| | | assert.match(roleIndexSource, /v-auth=\"'query'\"/) |
| | | assert.match(roleIndexSource, /auth: 'edit'/) |
| | | assert.match(roleIndexSource, /auth: 'delete'/) |
| | | }) |
| | | |
| | | test('createRoleSearchState exposes the role search form model', () => { |
| | | assert.deepEqual(createRoleSearchState(), { |
| | | name: '', |
| | | code: '', |
| | | memo: '', |
| | | status: void 0, |
| | | condition: '' |
| | | }) |
| | | }) |
| | |
| | | import assert from 'node:assert/strict' |
| | | import test from 'node:test' |
| | | import { buildUserDialogModel } from '../src/views/system/user/userPage.helpers.js' |
| | | |
| | | test('buildUserDialogModel maps rsf-admin edit data into the dialog model', () => { |
| | | assert.equal( |
| | | buildUserDialogModel({ username: 'root', nickname: '管理员', deptId: 1, roles: [{ id: 3 }] }).username, |
| | | 'root' |
| | | import { |
| | | buildUserDialogModel, |
| | | buildUserPageQueryParams, |
| | | buildUserSavePayload, |
| | | buildUserSearchParams, |
| | | getUserStatusMeta, |
| | | mergeUserDetailRecord, |
| | | normalizeDeptTreeOptions, |
| | | normalizeRoleOptions, |
| | | normalizeUserListRow |
| | | } from '../src/views/system/user/userPage.helpers.js' |
| | | |
| | | test('buildUserSearchParams keeps real user page search fields', () => { |
| | | assert.deepEqual( |
| | | buildUserSearchParams({ |
| | | username: 'root', |
| | | nickname: '管理员', |
| | | phone: '13800000000', |
| | | email: 'root@example.com', |
| | | status: 1, |
| | | deptId: 0, |
| | | roleIds: [3, 8], |
| | | code: 'A001', |
| | | sex: 1, |
| | | realName: 'Vincent', |
| | | idCard: '330421199511233211', |
| | | condition: 'root' |
| | | }), |
| | | { |
| | | username: 'root', |
| | | nickname: '管理员', |
| | | phone: '13800000000', |
| | | email: 'root@example.com', |
| | | status: 1, |
| | | deptId: 0, |
| | | roleIds: [3, 8], |
| | | code: 'A001', |
| | | sex: 1, |
| | | realName: 'Vincent', |
| | | idCard: '330421199511233211', |
| | | condition: 'root' |
| | | } |
| | | ) |
| | | }) |
| | | |
| | | test('buildUserPageQueryParams merges paging and search fields', () => { |
| | | assert.deepEqual( |
| | | buildUserPageQueryParams({ |
| | | current: 2, |
| | | size: 20, |
| | | username: 'root', |
| | | condition: 'manager' |
| | | }), |
| | | { |
| | | current: 2, |
| | | pageSize: 20, |
| | | username: 'root', |
| | | condition: 'manager' |
| | | } |
| | | ) |
| | | }) |
| | | |
| | | test('buildUserDialogModel normalizes backend edit data into the form model', () => { |
| | | assert.deepEqual( |
| | | buildUserDialogModel({ |
| | | id: 7, |
| | | username: 'root', |
| | | nickname: '管理员', |
| | | deptId: 1, |
| | | roles: [{ id: 3 }, { roleId: 8 }], |
| | | sex: 1, |
| | | code: 'A001', |
| | | phone: '13800000000', |
| | | email: 'root@example.com', |
| | | realName: 'Vincent', |
| | | idCard: '330421199511233211', |
| | | memo: 'memo', |
| | | status: 0 |
| | | }), |
| | | { |
| | | id: 7, |
| | | username: 'root', |
| | | nickname: '管理员', |
| | | deptId: 1, |
| | | roleIds: [3, 8], |
| | | userRoleIds: [3, 8], |
| | | password: '', |
| | | confirmPassword: '', |
| | | sex: 1, |
| | | code: 'A001', |
| | | phone: '13800000000', |
| | | email: 'root@example.com', |
| | | realName: 'Vincent', |
| | | idCard: '330421199511233211', |
| | | memo: 'memo', |
| | | status: 0 |
| | | } |
| | | ) |
| | | }) |
| | | |
| | | test('buildUserSavePayload submits roleIds and password for the backend', () => { |
| | | assert.deepEqual( |
| | | buildUserSavePayload({ |
| | | id: 7, |
| | | username: 'root', |
| | | nickname: '管理员', |
| | | deptId: 1, |
| | | userRoleIds: [3, 8], |
| | | password: 'secret', |
| | | confirmPassword: 'secret', |
| | | sex: 1, |
| | | code: 'A001', |
| | | phone: '13800000000', |
| | | email: 'root@example.com', |
| | | realName: 'Vincent', |
| | | idCard: '330421199511233211', |
| | | memo: 'memo', |
| | | status: 1 |
| | | }), |
| | | { |
| | | id: 7, |
| | | username: 'root', |
| | | nickname: '管理员', |
| | | deptId: 1, |
| | | roleIds: [3, 8], |
| | | password: 'secret', |
| | | sex: 1, |
| | | code: 'A001', |
| | | phone: '13800000000', |
| | | email: 'root@example.com', |
| | | realName: 'Vincent', |
| | | idCard: '330421199511233211', |
| | | memo: 'memo', |
| | | status: 1 |
| | | } |
| | | ) |
| | | }) |
| | | |
| | | test('normalizeUserListRow exposes table friendly fields', () => { |
| | | const normalized = normalizeUserListRow({ |
| | | username: 'root', |
| | | nickname: '管理员', |
| | | deptLabel: '研发部', |
| | | deptId$: '研发部', |
| | | roleNames: '超级管理员、OPS', |
| | | roles: [{ name: '超级管理员' }, { code: 'OPS' }], |
| | | status: 1, |
| | | createTime$: '2025-03-28 10:00:00', |
| | | updateTime: '2025-03-28 11:00:00' |
| | | }) |
| | | |
| | | assert.equal(normalized.username, 'root') |
| | | assert.equal(normalized.deptLabel, '研发部') |
| | | assert.equal(normalized.roleNames, '超级管理员、OPS') |
| | | assert.equal(normalized.statusBool, true) |
| | | assert.equal(normalized.statusText, '正常') |
| | | assert.equal(normalized.statusType, 'success') |
| | | assert.equal(normalized.createTimeText, '2025-03-28 10:00:00') |
| | | assert.equal(normalized.updateTimeText, '2025-03-28 11:00:00') |
| | | }) |
| | | |
| | | test('mergeUserDetailRecord keeps list row roles when detail omits them', () => { |
| | | assert.deepEqual( |
| | | mergeUserDetailRecord( |
| | | { |
| | | id: 7, |
| | | username: 'root', |
| | | nickname: '管理员' |
| | | }, |
| | | { |
| | | id: 7, |
| | | deptLabel: '研发部', |
| | | roleNames: '超级管理员', |
| | | roles: [{ id: 3, name: '超级管理员' }] |
| | | } |
| | | ), |
| | | { |
| | | id: 7, |
| | | username: 'root', |
| | | nickname: '管理员', |
| | | deptLabel: '研发部', |
| | | roleNames: '超级管理员', |
| | | roles: [{ id: 3, name: '超级管理员' }] |
| | | } |
| | | ) |
| | | }) |
| | | |
| | | test('normalizeDeptTreeOptions and normalizeRoleOptions adapt lookup data', () => { |
| | | assert.deepEqual( |
| | | normalizeDeptTreeOptions([{ id: 1, name: '总部', children: [{ id: 2, name: '研发部' }] }]), |
| | | [{ value: 1, label: '总部', children: [{ value: 2, label: '研发部', children: [] }] }] |
| | | ) |
| | | |
| | | assert.deepEqual( |
| | | normalizeRoleOptions([{ id: 3, name: '管理员' }, { roleId: 8, code: 'OPS' }]), |
| | | [ |
| | | { value: 3, label: '管理员' }, |
| | | { value: 8, label: 'OPS' } |
| | | ] |
| | | ) |
| | | }) |
| | | |
| | | test('getUserStatusMeta maps enabled and disabled states', () => { |
| | | assert.deepEqual(getUserStatusMeta(1), { type: 'success', text: '正常', bool: true }) |
| | | assert.deepEqual(getUserStatusMeta(0), { type: 'danger', text: '禁用', bool: false }) |
| | | }) |
| New file |
| | |
| | | import assert from 'node:assert/strict' |
| | | import test from 'node:test' |
| | | |
| | | const { createUserLoginApiParams, userLoginPaginationKey } = await import( |
| | | '../src/views/system/user-login/userLoginTable.config.js' |
| | | ) |
| | | |
| | | test('user login page uses the backend pageSize pagination contract', () => { |
| | | assert.deepEqual(userLoginPaginationKey, { |
| | | current: 'current', |
| | | size: 'pageSize' |
| | | }) |
| | | }) |
| | | |
| | | test('user login page builds initial params with pageSize instead of size', () => { |
| | | assert.deepEqual( |
| | | createUserLoginApiParams({ |
| | | token: 'abc', |
| | | ip: '127.0.0.1', |
| | | system: 'wms' |
| | | }), |
| | | { |
| | | current: 1, |
| | | pageSize: 20, |
| | | token: 'abc', |
| | | ip: '127.0.0.1', |
| | | system: 'wms' |
| | | } |
| | | ) |
| | | }) |
| New file |
| | |
| | | import assert from 'node:assert/strict' |
| | | import fs from 'node:fs' |
| | | import path from 'node:path' |
| | | import test from 'node:test' |
| | | import { fileURLToPath } from 'node:url' |
| | | |
| | | const __dirname = path.dirname(fileURLToPath(import.meta.url)) |
| | | const projectRoot = path.resolve(__dirname, '..') |
| | | const workTabSource = fs.readFileSync( |
| | | path.join(projectRoot, 'src/components/core/layouts/art-work-tab/index.vue'), |
| | | 'utf8' |
| | | ) |
| | | |
| | | test('work tabs only render the leading icon when a tab actually has an icon', () => { |
| | | assert.match(workTabSource, /<ArtSvgIcon\s+v-if="item\.icon"/) |
| | | assert.doesNotMatch(workTabSource, /<ArtSvgIcon\s+v-show="item\.icon"/) |
| | | }) |
| New file |
| | |
| | | import assert from 'node:assert/strict' |
| | | import fs from 'node:fs' |
| | | import path from 'node:path' |
| | | import test from 'node:test' |
| | | import { fileURLToPath } from 'node:url' |
| | | |
| | | const __dirname = path.dirname(fileURLToPath(import.meta.url)) |
| | | const projectRoot = path.resolve(__dirname, '..') |
| | | const worktabSource = fs.readFileSync( |
| | | path.join(projectRoot, 'src/store/modules/worktab.js'), |
| | | 'utf8' |
| | | ) |
| | | |
| | | test('worktab store normalizes persisted legacy icon names before rendering tabs', () => { |
| | | assert.match(worktabSource, /import\s+\{\s*normalizeIcon\s*\}\s+from\s+'@\/router\/adapters\/backendMenuAdapter\.js'/) |
| | | assert.match(worktabSource, /icon:\s*normalizeIcon\(/) |
| | | assert.match(worktabSource, /const validTabs = opened\.value\.filter\(\(tab\) => isTabRouteValid\(tab\)\)\.map\(normalizeTabState\)/) |
| | | assert.match(worktabSource, /opened\.value\s*=\s*validTabs/) |
| | | }) |
| | |
| | | server: { |
| | | port: Number(VITE_PORT), |
| | | proxy: { |
| | | '/api': { |
| | | '/rsf-server': { |
| | | target: VITE_API_PROXY_URL, |
| | | changeOrigin: true |
| | | } |