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('&', '&amp;')
+    .replaceAll('<', '&lt;')
+    .replaceAll('>', '&gt;')
+    .replaceAll('"', '&quot;')
+    .replaceAll("'", '&#39;')
+}
+
+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