From 40905cbd04c2e332cd4bc2b9e0c5b3e1da9cccfa Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 30 三月 2026 08:17:32 +0800
Subject: [PATCH] feat: complete rsf-design phase 1 integration
---
rsf-design/tests/system-role-scope-contract.test.mjs | 218 ++
rsf-design/src/api/system-manage.js | 391 ++++
rsf-design/src/components/biz/list-export-print/list-print-document.js | 282 +++
rsf-design/tests/work-tab-icon-contract.test.mjs | 17
rsf-design/tests/backend-api-base.test.mjs | 30
rsf-design/tests/system-menu-page-contract.test.mjs | 67
rsf-design/src/views/system/role/modules/role-permission-dialog.vue | 413 +++-
rsf-design/tests/navigation-home-path.test.mjs | 65
rsf-design/scripts/build-local-iconify-collections.mjs | 7
rsf-design/src/router/core/MenuProcessor.js | 1
rsf-design/.env.development | 6
rsf-design/src/views/system/menu/modules/menu-dialog.vue | 412 ++--
rsf-design/src/views/system/user-login/index.vue | 164 +
rsf-design/src/views/system/menu/index.vue | 335 ++-
rsf-design/.env.production | 2
rsf-design/tests/iconify-local-prefixes.test.mjs | 21
rsf-design/tests/user-login-page-contract.test.mjs | 30
rsf-design/src/store/modules/worktab.js | 28
rsf-design/tests/worktab-icon-normalization-contract.test.mjs | 19
rsf-design/src/components/core/layouts/art-work-tab/index.vue | 2
rsf-design/tests/system-manage-contract.test.mjs | 129 +
rsf-design/vite.config.js | 2
rsf-design/src/utils/navigation/route.js | 18
rsf-design/src/components/biz/list-export-print/list-export-print.helpers.js | 155 +
rsf-design/package.json | 1
rsf-design/src/router/adapters/backendMenuAdapter.js | 240 ++
rsf-design/src/plugins/iconify.collections.js | 328 ---
rsf-design/tests/list-print-preview-style-contract.test.mjs | 202 ++
rsf-design/src/components/biz/list-export-print/index.vue | 80
rsf-design/src/utils/backend-menu-title.js | 153 +
rsf-design/tests/system-user-page-contract.test.mjs | 209 ++
rsf-design/src/views/system/user-login/userLoginTable.config.js | 14
rsf-design/tests/iconify-local-minimal.test.mjs | 17
rsf-design/src/components/biz/list-export-print/list-print-preview-dialog.vue | 199 ++
rsf-design/src/views/system/user/userPage.helpers.js | 287 +++
rsf-design/src/views/system/user/modules/user-search.vue | 160 +
rsf-design/src/views/system/role/modules/role-edit-dialog.vue | 206 +
rsf-design/tests/backend-menu-adapter.test.mjs | 208 ++
38 files changed, 4,205 insertions(+), 913 deletions(-)
diff --git a/rsf-design/.env.development b/rsf-design/.env.development
index 36ddc4b..e68456a 100644
--- a/rsf-design/.env.development
+++ b/rsf-design/.env.development
@@ -3,11 +3,11 @@
# 搴旂敤閮ㄧ讲鍩虹璺緞锛堝閮ㄧ讲鍦ㄥ瓙鐩綍 /admin锛屽垯璁剧疆涓� /admin/锛�
VITE_BASE_URL = /
-# API 璇锋眰鍩虹璺緞锛堝紑鍙戠幆澧冭缃负 / 浣跨敤浠g悊锛岀敓浜х幆澧冭缃负瀹屾暣鍚庣鍦板潃锛�
-VITE_API_URL = /
+# API 璇锋眰鍩虹璺緞锛堝紑鍙戠幆澧冮�氳繃 rsf-server 涓婁笅鏂囪矾寰勮蛋浠g悊锛�
+VITE_API_URL = /rsf-server
# 浠g悊鐩爣鍦板潃锛堝紑鍙戠幆澧冮�氳繃 Vite 浠g悊杞彂璇锋眰鍒版鍦板潃锛岃В鍐宠法鍩熼棶棰橈級
-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
diff --git a/rsf-design/.env.production b/rsf-design/.env.production
index 5941f31..1051271 100644
--- a/rsf-design/.env.production
+++ b/rsf-design/.env.production
@@ -4,7 +4,7 @@
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
diff --git a/rsf-design/package.json b/rsf-design/package.json
index b5e96f7..f0db9dd 100644
--- a/rsf-design/package.json
+++ b/rsf-design/package.json
@@ -7,6 +7,7 @@
"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",
diff --git a/rsf-design/scripts/build-local-iconify-collections.mjs b/rsf-design/scripts/build-local-iconify-collections.mjs
index be0d8dc..ca02615 100644
--- a/rsf-design/scripts/build-local-iconify-collections.mjs
+++ b/rsf-design/scripts/build-local-iconify-collections.mjs
@@ -45,13 +45,18 @@
}
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)
diff --git a/rsf-design/src/api/system-manage.js b/rsf-design/src/api/system-manage.js
index 1c9f35a..f28243d 100644
--- a/rsf-design/src/api/system-manage.js
+++ b/rsf-design/src/api/system-manage.js
@@ -4,11 +4,12 @@
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 } : {})
}
}
@@ -16,10 +17,10 @@
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 } : {})
}
}
@@ -40,7 +41,8 @@
}
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) {
@@ -71,32 +73,145 @@
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) {
@@ -106,8 +221,242 @@
})
}
-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 {
@@ -123,9 +472,17 @@
fetchUpdateRole,
fetchDeleteRole,
fetchGetRoleOptions,
+ fetchRolePrintPage,
+ fetchRolePage,
+ fetchExportRoleReport,
+ fetchGetRoleMany,
fetchGetDeptTree,
fetchGetMenuTree,
+ fetchSaveMenu,
+ fetchUpdateMenu,
+ fetchDeleteMenu,
fetchGetRoleScopeList,
+ fetchGetRoleScopeTree,
fetchUpdateRoleScope,
fetchGetUserLoginList,
fetchGetMenuList
diff --git a/rsf-design/src/components/biz/list-export-print/index.vue b/rsf-design/src/components/biz/list-export-print/index.vue
new file mode 100644
index 0000000..b5fee68
--- /dev/null
+++ b/rsf-design/src/components/biz/list-export-print/index.vue
@@ -0,0 +1,80 @@
+<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>
diff --git a/rsf-design/src/components/biz/list-export-print/list-export-print.helpers.js b/rsf-design/src/components/biz/list-export-print/list-export-print.helpers.js
new file mode 100644
index 0000000..5a07f83
--- /dev/null
+++ b/rsf-design/src/components/biz/list-export-print/list-export-print.helpers.js
@@ -0,0 +1,155 @@
+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 }
diff --git a/rsf-design/src/components/biz/list-export-print/list-print-document.js b/rsf-design/src/components/biz/list-export-print/list-print-document.js
new file mode 100644
index 0000000..c1c1bc6
--- /dev/null
+++ b/rsf-design/src/components/biz/list-export-print/list-print-document.js
@@ -0,0 +1,282 @@
+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()
+}
diff --git a/rsf-design/src/components/biz/list-export-print/list-print-preview-dialog.vue b/rsf-design/src/components/biz/list-export-print/list-print-preview-dialog.vue
new file mode 100644
index 0000000..e0e8f10
--- /dev/null
+++ b/rsf-design/src/components/biz/list-export-print/list-print-preview-dialog.vue
@@ -0,0 +1,199 @@
+<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>
diff --git a/rsf-design/src/components/core/layouts/art-work-tab/index.vue b/rsf-design/src/components/core/layouts/art-work-tab/index.vue
index 8930b3e..aabe9c7 100644
--- a/rsf-design/src/components/core/layouts/art-work-tab/index.vue
+++ b/rsf-design/src/components/core/layouts/art-work-tab/index.vue
@@ -39,7 +39,7 @@
@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'"
diff --git a/rsf-design/src/plugins/iconify.collections.js b/rsf-design/src/plugins/iconify.collections.js
index 8c6428e..632d84d 100644
--- a/rsf-design/src/plugins/iconify.collections.js
+++ b/rsf-design/src/plugins/iconify.collections.js
@@ -12,68 +12,6 @@
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: {
@@ -117,35 +55,14 @@
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"/>'
@@ -156,23 +73,20 @@
'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"/>'
@@ -180,20 +94,8 @@
'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"/>'
@@ -204,29 +106,14 @@
'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"/>'
@@ -240,14 +127,8 @@
'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"/>'
@@ -255,20 +136,11 @@
'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"/>'
@@ -279,44 +151,26 @@
'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"/>'
@@ -324,20 +178,11 @@
'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"/>'
@@ -345,8 +190,8 @@
'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"/>'
@@ -354,14 +199,14 @@
'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"/>'
@@ -370,9 +215,6 @@
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': {
@@ -384,20 +226,11 @@
'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"/>'
@@ -411,20 +244,14 @@
'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"/>'
@@ -432,77 +259,47 @@
'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"/>'
@@ -519,23 +316,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',
diff --git a/rsf-design/src/router/adapters/backendMenuAdapter.js b/rsf-design/src/router/adapters/backendMenuAdapter.js
new file mode 100644
index 0000000..41219c9
--- /dev/null
+++ b/rsf-design/src/router/adapters/backendMenuAdapter.js
@@ -0,0 +1,240 @@
+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 }
diff --git a/rsf-design/src/router/core/MenuProcessor.js b/rsf-design/src/router/core/MenuProcessor.js
index 13b37e9..1a32f2f 100644
--- a/rsf-design/src/router/core/MenuProcessor.js
+++ b/rsf-design/src/router/core/MenuProcessor.js
@@ -166,6 +166,7 @@
*/
isValidAbsolutePath(path) {
return (
+ path.startsWith('/') ||
path.startsWith('http://') ||
path.startsWith('https://') ||
path.startsWith('/outside/iframe/')
diff --git a/rsf-design/src/store/modules/worktab.js b/rsf-design/src/store/modules/worktab.js
index 301c807..da66bf0 100644
--- a/rsf-design/src/store/modules/worktab.js
+++ b/rsf-design/src/store/modules/worktab.js
@@ -2,6 +2,19 @@
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',
() => {
@@ -53,7 +66,7 @@
}
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 {
@@ -62,7 +75,7 @@
current.value = newTab
} else {
const existingTab = opened.value[existingIndex]
- opened.value[existingIndex] = {
+ opened.value[existingIndex] = normalizeTabState({
...existingTab,
path: tab.path,
params: tab.params,
@@ -72,7 +85,7 @@
keepAlive: tab.keepAlive ?? existingTab.keepAlive,
name: tab.name || existingTab.name,
icon: tab.icon || existingTab.icon
- }
+ })
current.value = opened.value[existingIndex]
}
}
@@ -257,15 +270,22 @@
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 = {}
}
diff --git a/rsf-design/src/utils/backend-menu-title.js b/rsf-design/src/utils/backend-menu-title.js
new file mode 100644
index 0000000..4599d84
--- /dev/null
+++ b/rsf-design/src/utils/backend-menu-title.js
@@ -0,0 +1,153 @@
+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': '浠诲姟妗f槑缁�',
+ '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': '缁勬墭妗f槑缁�',
+ 'menu.waitPakinItemLog': '缁勬墭鍘嗗彶妗f槑缁�',
+ '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
+}
diff --git a/rsf-design/src/utils/navigation/route.js b/rsf-design/src/utils/navigation/route.js
index 822ab32..bdd1f47 100644
--- a/rsf-design/src/utils/navigation/route.js
+++ b/rsf-design/src/utils/navigation/route.js
@@ -1,6 +1,11 @@
+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
@@ -13,6 +18,15 @@
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 ''
@@ -27,7 +41,9 @@
return childPath
}
}
- return normalizePath(menuItem.path)
+ if (hasReleasedComponent(menuItem)) {
+ return normalizePath(menuItem.path)
+ }
}
return ''
}
diff --git a/rsf-design/src/views/system/menu/index.vue b/rsf-design/src/views/system/menu/index.vue
index 3a79c27..2bb409c 100644
--- a/rsf-design/src/views/system/menu/index.vue
+++ b/rsf-design/src/views/system/menu/index.vue
@@ -1,7 +1,6 @@
<!-- 鑿滃崟绠$悊椤甸潰 -->
<template>
<div class="menu-page art-full-height">
- <!-- 鎼滅储鏍� -->
<ArtSearchBar
v-model="formFilters"
:items="formItems"
@@ -11,7 +10,6 @@
/>
<ElCard class="art-table-card">
- <!-- 琛ㄦ牸澶撮儴 -->
<ArtTableHeader
:showZebra="false"
:loading="loading"
@@ -19,7 +17,7 @@
@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>
@@ -28,7 +26,7 @@
<ArtTable
ref="tableRef"
- rowKey="path"
+ rowKey="id"
:loading="loading"
:columns="columns"
:data="filteredTableData"
@@ -37,12 +35,12 @@
:default-expand-all="false"
/>
- <!-- 鑿滃崟寮圭獥 -->
<MenuDialog
v-model:visible="dialogVisible"
:type="dialogType"
:editData="editData"
:lockType="lockMenuType"
+ :menuTreeOptions="menuTreeOptions"
@submit="handleSubmit"
/>
</ElCard>
@@ -56,22 +54,34 @@
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: '鑿滃崟鍚嶇О',
@@ -86,44 +96,88 @@
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: '鍥炬爣棰勮',
@@ -134,52 +188,64 @@
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',
@@ -187,7 +253,7 @@
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, {
@@ -196,14 +262,14 @@
}),
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, {
@@ -212,25 +278,13 @@
}),
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)
@@ -243,6 +297,7 @@
}
return cloned
}
+
const convertAuthListToChildren = (items) => {
return items.map((item) => {
const clonedItem = deepClone(item)
@@ -251,13 +306,17 @@
}
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
@@ -267,15 +326,17 @@
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) {
@@ -285,77 +346,147 @@
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(() => {
diff --git a/rsf-design/src/views/system/menu/modules/menu-dialog.vue b/rsf-design/src/views/system/menu/modules/menu-dialog.vue
index 7c928de..c2a5dc9 100644
--- a/rsf-design/src/views/system/menu/modules/menu-dialog.vue
+++ b/rsf-design/src/views/system/menu/modules/menu-dialog.vue
@@ -3,7 +3,7 @@
:title="dialogTitle"
:model-value="visible"
@update:model-value="handleCancel"
- width="860px"
+ width="760px"
align-center
class="menu-dialog"
@closed="handleClosed"
@@ -13,7 +13,7 @@
v-model="form"
:items="formItems"
:rules="rules"
- :span="width > 640 ? 12 : 24"
+ :span="24"
:gutter="20"
label-width="100px"
:show-reset="false"
@@ -21,16 +21,16 @@
>
<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>
@@ -39,248 +39,252 @@
<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銆乽ser锛�'
- ),
- 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銆丷_ADMIN锛塡n鍚庣鏉冮檺妯″紡锛氭棤闇�閰嶇疆'
- ),
- key: 'roles',
- type: 'inputtag',
- props: { placeholder: '杈撳叆瑙掕壊鏍囪瘑鍚庢寜鍥炶溅锛屽锛歊_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銆丠ot' }
- },
- {
- 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銆乪dit銆乨elete' }
- },
- {
- 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()
}
}
)
diff --git a/rsf-design/src/views/system/role/modules/role-edit-dialog.vue b/rsf-design/src/views/system/role/modules/role-edit-dialog.vue
index 08d3e41..713149b 100644
--- a/rsf-design/src/views/system/role/modules/role-edit-dialog.vue
+++ b/rsf-design/src/views/system/role/modules/role-edit-dialog.vue
@@ -1,109 +1,147 @@
<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: '姝e父', 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>
diff --git a/rsf-design/src/views/system/role/modules/role-permission-dialog.vue b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
index 0940fab..552ba28 100644
--- a/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
+++ b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
@@ -1,153 +1,304 @@
<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>
diff --git a/rsf-design/src/views/system/user-login/index.vue b/rsf-design/src/views/system/user-login/index.vue
new file mode 100644
index 0000000..140143d
--- /dev/null
+++ b/rsf-design/src/views/system/user-login/index.vue
@@ -0,0 +1,164 @@
+<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>
diff --git a/rsf-design/src/views/system/user-login/userLoginTable.config.js b/rsf-design/src/views/system/user-login/userLoginTable.config.js
new file mode 100644
index 0000000..11e4144
--- /dev/null
+++ b/rsf-design/src/views/system/user-login/userLoginTable.config.js
@@ -0,0 +1,14 @@
+function createUserLoginApiParams(filters = {}) {
+ return {
+ current: 1,
+ pageSize: 20,
+ ...filters
+ }
+}
+
+const userLoginPaginationKey = {
+ current: 'current',
+ size: 'pageSize'
+}
+
+export { createUserLoginApiParams, userLoginPaginationKey }
diff --git a/rsf-design/src/views/system/user/modules/user-search.vue b/rsf-design/src/views/system/user/modules/user-search.vue
index 071a0f9..849a4bc 100644
--- a/rsf-design/src/views/system/user/modules/user-search.vue
+++ b/rsf-design/src/views/system/user/modules/user-search.vue
@@ -3,61 +3,143 @@
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: '鐘舵��',
@@ -65,28 +147,22 @@
type: 'select',
props: {
placeholder: '璇烽�夋嫨鐘舵��',
- options: statusOptions.value
- }
- },
- {
- label: '鎬у埆',
- key: 'userGender',
- type: 'radiogroup',
- props: {
+ clearable: true,
options: [
- { label: '鐢�', value: '1' },
- { label: '濂�', value: '2' }
+ { label: '姝e父', 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>
diff --git a/rsf-design/src/views/system/user/userPage.helpers.js b/rsf-design/src/views/system/user/userPage.helpers.js
new file mode 100644
index 0000000..a2173a2
--- /dev/null
+++ b/rsf-design/src/views/system/user/userPage.helpers.js
@@ -0,0 +1,287 @@
+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: '姝e父', 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
+}
diff --git a/rsf-design/tests/backend-api-base.test.mjs b/rsf-design/tests/backend-api-base.test.mjs
new file mode 100644
index 0000000..ded0f78
--- /dev/null
+++ b/rsf-design/tests/backend-api-base.test.mjs
@@ -0,0 +1,30 @@
+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/)
+})
diff --git a/rsf-design/tests/backend-menu-adapter.test.mjs b/rsf-design/tests/backend-menu-adapter.test.mjs
new file mode 100644
index 0000000..76d6c49
--- /dev/null
+++ b/rsf-design/tests/backend-menu-adapter.test.mjs
@@ -0,0 +1,208 @@
+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`)
+ })
+})
diff --git a/rsf-design/tests/iconify-local-minimal.test.mjs b/rsf-design/tests/iconify-local-minimal.test.mjs
index 93d9975..b3925d5 100644
--- a/rsf-design/tests/iconify-local-minimal.test.mjs
+++ b/rsf-design/tests/iconify-local-minimal.test.mjs
@@ -23,13 +23,28 @@
}
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)
diff --git a/rsf-design/tests/iconify-local-prefixes.test.mjs b/rsf-design/tests/iconify-local-prefixes.test.mjs
index 83b7163..7041182 100644
--- a/rsf-design/tests/iconify-local-prefixes.test.mjs
+++ b/rsf-design/tests/iconify-local-prefixes.test.mjs
@@ -23,14 +23,29 @@
}
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)
}
}
diff --git a/rsf-design/tests/list-print-preview-style-contract.test.mjs b/rsf-design/tests/list-print-preview-style-contract.test.mjs
new file mode 100644
index 0000000..760c0e1
--- /dev/null
+++ b/rsf-design/tests/list-print-preview-style-contract.test.mjs
@@ -0,0 +1,202 @@
+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'"))
+})
diff --git a/rsf-design/tests/navigation-home-path.test.mjs b/rsf-design/tests/navigation-home-path.test.mjs
new file mode 100644
index 0000000..ad5e873
--- /dev/null
+++ b/rsf-design/tests/navigation-home-path.test.mjs
@@ -0,0 +1,65 @@
+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')
+})
diff --git a/rsf-design/tests/system-manage-contract.test.mjs b/rsf-design/tests/system-manage-contract.test.mjs
index c9879e9..f542a6b 100644
--- a/rsf-design/tests/system-manage-contract.test.mjs
+++ b/rsf-design/tests/system-manage-contract.test.mjs
@@ -1,20 +1,147 @@
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)
+})
diff --git a/rsf-design/tests/system-menu-page-contract.test.mjs b/rsf-design/tests/system-menu-page-contract.test.mjs
index d8bb894..4592ee2 100644
--- a/rsf-design/tests/system-menu-page-contract.test.mjs
+++ b/rsf-design/tests/system-menu-page-contract.test.mjs
@@ -1,37 +1,56 @@
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')
})
diff --git a/rsf-design/tests/system-role-scope-contract.test.mjs b/rsf-design/tests/system-role-scope-contract.test.mjs
index 30c293e..dd8b2af 100644
--- a/rsf-design/tests/system-role-scope-contract.test.mjs
+++ b/rsf-design/tests/system-role-scope-contract.test.mjs
@@ -1,21 +1,211 @@
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, '姝e父')
+ 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: ''
+ })
})
diff --git a/rsf-design/tests/system-user-page-contract.test.mjs b/rsf-design/tests/system-user-page-contract.test.mjs
index 3c09037..37da9ce 100644
--- a/rsf-design/tests/system-user-page-contract.test.mjs
+++ b/rsf-design/tests/system-user-page-contract.test.mjs
@@ -1,10 +1,209 @@
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: '瓒呯骇绠$悊鍛樸�丱PS',
+ 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, '瓒呯骇绠$悊鍛樸�丱PS')
+ assert.equal(normalized.statusBool, true)
+ assert.equal(normalized.statusText, '姝e父')
+ 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: '姝e父', bool: true })
+ assert.deepEqual(getUserStatusMeta(0), { type: 'danger', text: '绂佺敤', bool: false })
+})
diff --git a/rsf-design/tests/user-login-page-contract.test.mjs b/rsf-design/tests/user-login-page-contract.test.mjs
new file mode 100644
index 0000000..4291b8a
--- /dev/null
+++ b/rsf-design/tests/user-login-page-contract.test.mjs
@@ -0,0 +1,30 @@
+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'
+ }
+ )
+})
diff --git a/rsf-design/tests/work-tab-icon-contract.test.mjs b/rsf-design/tests/work-tab-icon-contract.test.mjs
new file mode 100644
index 0000000..4e1111c
--- /dev/null
+++ b/rsf-design/tests/work-tab-icon-contract.test.mjs
@@ -0,0 +1,17 @@
+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"/)
+})
diff --git a/rsf-design/tests/worktab-icon-normalization-contract.test.mjs b/rsf-design/tests/worktab-icon-normalization-contract.test.mjs
new file mode 100644
index 0000000..510c4b8
--- /dev/null
+++ b/rsf-design/tests/worktab-icon-normalization-contract.test.mjs
@@ -0,0 +1,19 @@
+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/)
+})
diff --git a/rsf-design/vite.config.js b/rsf-design/vite.config.js
index 9963fb9..e1dd2a1 100644
--- a/rsf-design/vite.config.js
+++ b/rsf-design/vite.config.js
@@ -28,7 +28,7 @@
server: {
port: Number(VITE_PORT),
proxy: {
- '/api': {
+ '/rsf-server': {
target: VITE_API_PROXY_URL,
changeOrigin: true
}
--
Gitblit v1.9.1