编辑 | blame | 历史 | 原始文档

rsf-design Replace rsf-admin Phase 1 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make rsf-design independently handle login, auth state, user info, menu loading, dynamic routing, homepage, and button permissions against the existing rsf-server APIs while rsf-admin remains available as the old entry.

Architecture: Replace the Art Design Pro template auth/menu assumptions with the real rsf-admin protocol. Introduce one backend-menu adapter that converts /auth/menu payloads into the route shape expected by the existing dynamic router, and keep unpublished resources out of the new menu entirely. Store token, user info, roles, and button permissions in the existing Pinia stores and drive guards/directives from that single source of truth.

Tech Stack: Vue 3, Vite, Pinia, Vue Router, Element Plus, Art Design Pro, Axios, Node node:test, pnpm


File Map

  • Create: rsf-design/src/router/adapters/backendMenuAdapter.js
  • Create: rsf-design/tests/backend-menu-adapter.test.mjs
  • Create: rsf-design/tests/auth-contract.test.mjs
  • Modify: rsf-design/package.json
  • Modify: rsf-design/src/api/auth.js
  • Modify: rsf-design/src/store/modules/user.js
  • Modify: rsf-design/src/views/auth/login/index.vue
  • Modify: rsf-design/src/router/core/MenuProcessor.js
  • Modify: rsf-design/src/router/core/ComponentLoader.js
  • Modify: rsf-design/src/router/guards/beforeEach.js
  • Modify: rsf-design/src/hooks/core/useAuth.js
  • Modify: rsf-design/src/directives/core/auth.js
  • Modify: rsf-design/src/directives/core/roles.js
  • Modify: rsf-design/src/config/index.js
  • Modify: rsf-design/src/views/dashboard/console/index.vue
  • Optional verify-only reads: rsf-admin/src/api/auth/index.js, rsf-admin/src/page/ResourceContent.js, docs/superpowers/specs/2026-03-28-rsf-design-replace-rsf-admin-design.md

Task 1: Establish a Minimal Test Harness for the New Protocol Path

Files:
- Modify: rsf-design/package.json
- Create: rsf-design/tests/auth-contract.test.mjs
- Create: rsf-design/tests/backend-menu-adapter.test.mjs

  • [ ] Step 1: Write the failing auth contract test
import assert from 'node:assert/strict'
import test from 'node:test'
import { buildLoginPayload, normalizeLoginResponse } from '../src/api/auth.js'

test('buildLoginPayload matches the rsf-server login contract', () => {
  assert.deepEqual(buildLoginPayload({ username: 'demo', password: '123456' }), {
    username: 'demo',
    password: '123456'
  })
})

test('normalizeLoginResponse extracts the real token fields', () => {
  assert.deepEqual(
    normalizeLoginResponse({
      code: 200,
      data: { accessToken: 'abc', user: { id: 1 } }
    }),
    { accessToken: 'abc', refreshToken: '', user: { id: 1 } }
  )
})
  • [ ] Step 2: Run the auth contract test to verify it fails

Run: node --test rsf-design/tests/auth-contract.test.mjs
Expected: FAIL because buildLoginPayload and normalizeLoginResponse do not exist yet.

  • [ ] Step 3: Write the failing backend menu adapter test
import assert from 'node:assert/strict'
import test from 'node:test'
import { adaptBackendMenuTree } from '../src/router/adapters/backendMenuAdapter.js'

test('adapts a backend menu leaf into a routable phase-1 page', () => {
  const result = adaptBackendMenuTree([
    {
      id: 10,
      path: 'system',
      name: 'System',
      children: [
        { id: 11, path: 'user-login', component: 'userLogin', title: '登录日志' }
      ]
    }
  ])

  assert.equal(result[0].children[0].component, '/system/user-login')
})
  • [ ] Step 4: Run the backend menu adapter test to verify it fails

Run: node --test rsf-design/tests/backend-menu-adapter.test.mjs
Expected: FAIL because the adapter module does not exist yet.

  • [ ] Step 5: Add a stable repo-local test command
{
  "scripts": {
    "test": "node --test tests"
  }
}
  • [ ] Step 6: Re-run both tests from the rsf-design root

Run: pnpm test
Expected: FAIL, but the command should execute the new test files instead of erroring due to a missing script.

  • [ ] Step 7: Commit
git add rsf-design/package.json rsf-design/tests/auth-contract.test.mjs rsf-design/tests/backend-menu-adapter.test.mjs
git commit -m "test: add rsf-design phase-1 protocol tests"

Task 2: Replace Template Auth API Assumptions with the Real rsf-server Contract

Files:
- Modify: rsf-design/src/api/auth.js
- Modify: rsf-design/src/store/modules/user.js
- Test: rsf-design/tests/auth-contract.test.mjs

  • [ ] Step 1: Make the auth contract test more explicit before implementation
test('normalizeUserInfo returns roles and buttons arrays safely', () => {
  assert.deepEqual(
    normalizeUserInfo({ userId: 1, roles: ['R_ADMIN'], buttons: ['add'] }),
    { userId: 1, roles: ['R_ADMIN'], buttons: ['add'] }
  )
})
  • [ ] Step 2: Run the auth contract test again

Run: node --test rsf-design/tests/auth-contract.test.mjs
Expected: FAIL with missing exports or assertion mismatch.

  • [ ] Step 3: Implement the minimal real-contract helpers and API calls
export function buildLoginPayload({ username, password }) {
  return { username, password }
}

export function normalizeLoginResponse(payload) {
  return {
    accessToken: payload?.data?.accessToken || '',
    refreshToken: payload?.data?.refreshToken || '',
    user: payload?.data?.user || {}
  }
}

export function normalizeUserInfo(data) {
  return {
    ...data,
    roles: Array.isArray(data?.roles) ? data.roles : [],
    buttons: Array.isArray(data?.buttons) ? data.buttons : []
  }
}
  • [ ] Step 4: Point the auth API at the real endpoints
function fetchLogin(params) {
  return request.post({ url: '/login', params: buildLoginPayload(params) })
}

function fetchGetUserInfo() {
  return request.get({ url: '/auth/user' })
}

function fetchGetMenuList() {
  return request.get({ url: '/auth/menu' })
}
  • [ ] Step 5: Update the user store token setters to persist the real fields
const setToken = (newAccessToken, newRefreshToken = '') => {
  accessToken.value = newAccessToken || ''
  refreshToken.value = newRefreshToken || ''
}
  • [ ] Step 6: Run the auth contract test to verify it passes

Run: node --test rsf-design/tests/auth-contract.test.mjs
Expected: PASS.

  • [ ] Step 7: Commit
git add rsf-design/src/api/auth.js rsf-design/src/store/modules/user.js rsf-design/tests/auth-contract.test.mjs
git commit -m "feat: align rsf-design auth api with rsf-server"

Task 3: Rebuild the Login Flow Around the Real Contract

Files:
- Modify: rsf-design/src/views/auth/login/index.vue
- Modify: rsf-design/src/config/index.js
- Test: rsf-design/tests/auth-contract.test.mjs

  • [ ] Step 1: Write a failing login-page-oriented auth test
test('normalizeLoginResponse accepts the old backend accessToken shape', () => {
  const result = normalizeLoginResponse({
    code: 200,
    data: { accessToken: 'token-1', refreshToken: '', user: { username: 'admin' } }
  })
  assert.equal(result.accessToken, 'token-1')
})
  • [ ] Step 2: Run the auth contract test

Run: node --test rsf-design/tests/auth-contract.test.mjs
Expected: PASS if the previous task is complete. Use this as a safety baseline before editing the login page.

  • [ ] Step 3: Remove template-only login behavior
const formData = reactive({
  username: '',
  password: '',
  rememberPassword: true
})

const handleSubmit = async () => {
  const payload = normalizeLoginResponse(await fetchLogin(formData))
  userStore.setToken(payload.accessToken, payload.refreshToken)
  userStore.setLoginStatus(true)
  if (payload.user && Object.keys(payload.user).length > 0) userStore.setUserInfo(payload.user)
  router.push(route.query.redirect || '/')
}
  • [ ] Step 4: Remove demo account selectors and align visible copy
systemInfo: {
  name: 'RSF Design'
}
  • [ ] Step 5: Run lint on the edited login and config files

Run: pnpm lint rsf-design/src/views/auth/login/index.vue rsf-design/src/config/index.js
Expected: PASS, or no syntax errors if the lint command needs to be run as pnpm lint.

  • [ ] Step 6: Run a quick build smoke test

Run: pnpm --dir rsf-design build
Expected: PASS.

  • [ ] Step 7: Commit
git add rsf-design/src/views/auth/login/index.vue rsf-design/src/config/index.js
git commit -m "feat: rebuild rsf-design login flow for rsf-server"

Task 4: Introduce the Backend Menu Adapter and Drive Dynamic Routing From It

Files:
- Create: rsf-design/src/router/adapters/backendMenuAdapter.js
- Modify: rsf-design/src/router/core/MenuProcessor.js
- Modify: rsf-design/src/router/core/ComponentLoader.js
- Test: rsf-design/tests/backend-menu-adapter.test.mjs

  • [ ] Step 1: Expand the failing backend menu adapter test with realistic constraints
test('drops unpublished resources from the phase-1 menu tree', () => {
  const result = adaptBackendMenuTree([
    {
      id: 1,
      path: 'system',
      children: [
        { id: 2, path: 'user-login', component: 'userLogin', title: '登录日志' },
        { id: 3, path: 'host', component: 'host', title: '主机管理' }
      ]
    }
  ])

  assert.equal(result[0].children.length, 1)
  assert.equal(result[0].children[0].meta.title, '登录日志')
})
  • [ ] Step 2: Run the backend menu adapter test

Run: node --test rsf-design/tests/backend-menu-adapter.test.mjs
Expected: FAIL.

  • [ ] Step 3: Implement the adapter with an explicit phase-1 resource map
const PHASE_1_COMPONENTS = {
  console: '/dashboard/console',
  user: '/system/user',
  role: '/system/role',
  menu: '/system/menu',
  userLogin: '/system/user-login'
}

export function adaptBackendMenuTree(menuTree) {
  // normalize ids, names, meta, child paths, and component mappings here
}
  • [ ] Step 4: Change MenuProcessor to fetch /auth/menu through the auth API and hand off to the adapter
async processBackendMenu() {
  const list = await fetchGetMenuList()
  return adaptBackendMenuTree(list)
}
  • [ ] Step 5: Make ComponentLoader treat adapted component strings as final Vue view paths only
load(componentPath) {
  const fullPath = `../../views${componentPath}.vue`
  const fullPathWithIndex = `../../views${componentPath}/index.vue`
  // no resource-name guessing here
}
  • [ ] Step 6: Run the backend adapter test to verify it passes

Run: node --test rsf-design/tests/backend-menu-adapter.test.mjs
Expected: PASS.

  • [ ] Step 7: Commit
git add rsf-design/src/router/adapters/backendMenuAdapter.js rsf-design/src/router/core/MenuProcessor.js rsf-design/src/router/core/ComponentLoader.js rsf-design/tests/backend-menu-adapter.test.mjs
git commit -m "feat: adapt backend menus for rsf-design dynamic routing"

Task 5: Wire the Guard, Permissions, and Homepage to the New Data Flow

Files:
- Modify: rsf-design/src/router/guards/beforeEach.js
- Modify: rsf-design/src/hooks/core/useAuth.js
- Modify: rsf-design/src/directives/core/auth.js
- Modify: rsf-design/src/directives/core/roles.js
- Modify: rsf-design/src/views/dashboard/console/index.vue

  • [ ] Step 1: Add a failing permission-focused adapter/auth test
test('normalizeUserInfo preserves buttons for permission checks', () => {
  const result = normalizeUserInfo({ buttons: ['add', 'edit'] })
  assert.deepEqual(result.buttons, ['add', 'edit'])
})
  • [ ] Step 2: Run the auth contract test

Run: node --test rsf-design/tests/auth-contract.test.mjs
Expected: PASS before editing permission consumers.

  • [ ] Step 3: Update the route guard to fetch user info and menu from the real chain only once per session rebuild
const data = normalizeUserInfo(await fetchGetUserInfo())
userStore.setUserInfo(data)
const menuList = await menuProcessor.getMenuList()
  • [ ] Step 4: Make useAuth and directives read from store-backed user info and current route meta consistently
const backendAuthList = Array.isArray(route.meta.authList) ? route.meta.authList : []
const frontendAuthList = Array.isArray(info.value?.buttons) ? info.value.buttons : []
  • [ ] Step 5: Replace the template dashboard copy with an RSF phase-1 landing page
<template>
  <div class="art-full-height">
    <ElRow :gutter="20" class="mb-5">
      <ElCol :xs="24" :md="8">
        <ArtStatsCard title="当前阶段" count="1" description="运行骨架替换" icon="ri:rocket-line" />
      </ElCol>
    </ElRow>
  </div>
</template>
  • [ ] Step 6: Run build and the full test suite

Run: pnpm --dir rsf-design test
Expected: PASS.

Run: pnpm --dir rsf-design build
Expected: PASS.

  • [ ] Step 7: Commit
git add rsf-design/src/router/guards/beforeEach.js rsf-design/src/hooks/core/useAuth.js rsf-design/src/directives/core/auth.js rsf-design/src/directives/core/roles.js rsf-design/src/views/dashboard/console/index.vue
git commit -m "feat: wire rsf-design phase-1 auth, permissions, and homepage"

Task 6: Verify the Parallel-Run Entry and Record the Manual Acceptance Checks

Files:
- Modify: docs/superpowers/specs/2026-03-28-rsf-design-replace-rsf-admin-design.md (only if implementation changes the agreed contract)
- Verify: rsf-design/.env
- Verify: rsf-design/vite.config.js

  • [ ] Step 1: Confirm the new entry still runs in backend mode

Run: Get-Content rsf-design/.env
Expected: VITE_ACCESS_MODE = backend

  • [ ] Step 2: Start the new frontend locally

Run: pnpm --dir rsf-design dev
Expected: Vite starts successfully and prints the local URL.

  • [ ] Step 3: Manually verify the phase-1 acceptance path

Checklist:
- Login with a real rsf-server account
- Refresh after login and verify the menu rebuilds
- Visit a protected route directly and verify it resolves after route registration
- Verify hidden buttons remain hidden for a low-permission account
- Verify an unpublished resource does not appear in the new menu
- Verify rsf-admin still runs from its old entry unchanged

  • [ ] Step 4: Run the final automated verification

Run: pnpm --dir rsf-design test && pnpm --dir rsf-design build
Expected: PASS.

  • [ ] Step 5: Commit
git add rsf-design/.env rsf-design/vite.config.js
git commit -m "chore: verify rsf-design phase-1 replacement readiness"