Files
fangke/docs/superpowers/plans/2026-09-17-login-keji-visual-v1.md
T

16 KiB
Raw Blame History

登录页客迹视觉 V1 对齐 — 实现计划

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: 将 admin 登录页从 Art Design Pro 模板原版重做为客迹视觉 V1(深蓝品牌区 + 浅色表单区双栏),对齐设计稿 assets/design/keji-admin-v1/03-login.png。

Architecture: 重写三页共用组件 LoginLeftView.vue(背景图 + logo + 标语叠加层),简化 login/index.vue(移除 AuthTopBar/账号下拉/注册与忘记密码链接)并重写 style.css(固定浅色 + Element Plus 变量覆盖防暗色污染),同步更新 zh/en i18n 文案。

Tech Stack: Vue3 + TS + Element Plus + Tailwind(Art Design Pro 模板),Playwright 视觉验证。

Spec: docs/superpowers/specs/2026-09-17-login-keji-visual-v1-design.md(含全部决策记录与色值)

TDD 说明: 纯视觉重构、登录逻辑零改动(spec 明确),无新增单元测试。验证 = Playwright 截图对照设计稿 + pnpm build + 现有测试不受影响。

工作目录: 全程在 admin/ 下操作(git 仓库根为 D:\project\cms-cj,分支 m1)。


Task 1: i18n 文案更新

Files:

  • Modify: admin/src/locales/langs/zh.json:151-179(login 对象)

  • Modify: admin/src/locales/langs/en.json:151-179(login 对象)

  • Step 1: 更新 zh.json 的 login 对象

将 zh.json 中 "login" 对象内以下 key 的值替换(其余 key 原样保留):

"leftView": {
  "title": "看见来访,读懂客迹",
  "subTitle": "网站访问与客户旅程分析",
  "slogan": "数据驱动增长 · 洞察创造价值",
  "footer": "让每一次来访都有价值"
},
"title": "欢迎登录客迹",
"subTitle": "登录您的独立数据工作台",

并在 "placeholder" 对象同级新增三个 key(放在 "btnText" 之后):

"accountLabel": "账号",
"passwordLabel": "密码",
"adminAssigned": "账号由管理员分配",

同时把 "rememberPwd": "记住密码" 改为 "rememberPwd": "保持登录"。

删除 "noAccount" 和 "register" 两个 key(入口已移除)。

  • Step 2: 更新 en.json 的 login 对象

en.json 同结构对应更新:

"leftView": {
  "title": "See every visit, understand every journey",
  "subTitle": "Website visit & customer journey analytics",
  "slogan": "Data-driven growth · Insight-driven value",
  "footer": "Make every visit count"
},
"title": "Welcome to KEJI",
"subTitle": "Sign in to your data workspace",

新增:

"accountLabel": "Account",
"passwordLabel": "Password",
"adminAssigned": "Accounts are assigned by the administrator",

"rememberPwd": "Stay signed in";删除 "noAccount"、"register"。

注意: en.json 删除 noAccount/register 后,register/index.vue 等其他文件若引用 $t('login.register') 会显示 key 本身。先全局搜索确认只有 login/index.vue 引用(本任务后 Task 3 会移除该引用;若搜索发现其他引用处则保留 key 不删)。

  • Step 3: 验证 JSON 合法

Run: python -c "import json; json.load(open('src/locales/langs/zh.json', encoding='utf-8')); json.load(open('src/locales/langs/en.json', encoding='utf-8')); print('OK')" Expected: OK

  • Step 4: Commit
git add src/locales/langs/zh.json src/locales/langs/en.json
git commit -m "feat(admin): 登录页i18n文案对齐客迹V1"

Task 2: 重写 LoginLeftView.vue(左侧品牌区)

Files:

  • Modify: admin/src/components/core/views/login/LoginLeftView.vue(整文件重写)

资源(已存在,无需新增):src/assets/images/logo-w.png(1944×809 透明底白字)、src/assets/images/login/login-left-background.png(1196×1315)。

  • Step 1: 整文件替换为以下内容
<!-- 登录、注册、忘记密码左侧品牌区(客迹视觉V1) -->
<template>
  <div class="login-left-view" :style="{ backgroundImage: `url(${bg})` }">
    <img class="logo" :src="logoWhite" alt="客迹 KEJI ANALYTICS" />

    <div class="text-wrap">
      <h1>{{ $t('login.leftView.title') }}</h1>
      <p class="sub-title">{{ $t('login.leftView.subTitle') }}</p>
      <span class="accent-bar"></span>
      <p class="slogan">{{ $t('login.leftView.slogan') }}</p>
    </div>

    <p class="footer">{{ $t('login.leftView.footer') }}</p>
  </div>
</template>

<script setup lang="ts">
  import bg from '@imgs/login/login-left-background.png'
  import logoWhite from '@imgs/logo-w.png'
</script>

<style lang="scss" scoped>
  .login-left-view {
    position: relative;
    box-sizing: border-box;
    width: 58vw;
    height: 100%;
    overflow: hidden;
    background-color: #0b1630; /* 图片加载前/失败兜底 */
    background-position: center;
    background-size: cover;
    background-repeat: no-repeat;

    .logo {
      position: absolute;
      top: 56px;
      left: 64px;
      z-index: 10;
      display: block;
      width: auto;
      height: 52px;
    }

    .text-wrap {
      position: absolute;
      top: 24%;
      left: 64px;
      z-index: 10;

      h1 {
        max-width: 80%;
        font-size: 38px;
        font-weight: 700;
        color: #fff;
        letter-spacing: 2px;
      }

      .sub-title {
        margin-top: 14px;
        font-size: 18px;
        color: #dbe6f5;
      }

      .accent-bar {
        display: block;
        width: 50px;
        height: 6px;
        margin-top: 24px;
        background: #2563eb;
        border-radius: 3px;
      }

      .slogan {
        margin-top: 24px;
        font-size: 14px;
        color: #a8c0e8;
        letter-spacing: 4px;
      }
    }

    .footer {
      position: absolute;
      bottom: 56px;
      left: 64px;
      z-index: 10;
      font-size: 14px;
      color: #7d94c4;
      letter-spacing: 8px;
    }

    @media only screen and (width <= 1180px) {
      display: none;
    }
  }
</style>

要点:背景图走 JS import + inline style(Vite 哈希 + 懒加载友好);hideContent prop 删除(已确认无调用方);<=1600px 中间宽度规则不再保留,仅 58vw 一档;<=1180px 直接 display:none。

  • Step 2: 类型检查编译通过

Run: pnpm typecheck || pnpm run build(若项目无 typecheck 脚本则跳过,Task 5 统一 build 验证) Expected: 无错误(LoginLeftView 无外部引用变化,register/forget-password 自动获得新品牌区)

  • Step 3: Commit
git add src/components/core/views/login/LoginLeftView.vue
git commit -m "feat(admin): 登录左侧品牌区重做为客迹V1深蓝视觉"

Task 3: 简化 login/index.vue + 重写 style.css(右侧表单)

Files:

  • Modify: admin/src/views/auth/login/index.vue(模板 + script + style)

  • Modify: admin/src/views/auth/login/style.css(整文件重写)

  • Step 1: 替换 index.vue 的 template 部分

<!-- 登录页面 -->
<template>
  <div class="flex w-full h-screen">
    <LoginLeftView />

    <div class="relative flex-1 auth-right-bg">
      <div class="auth-right-wrap">
        <div class="form">
          <h3 class="title">{{ $t('login.title') }}</h3>
          <p class="sub-title">{{ $t('login.subTitle') }}</p>
          <ElForm
            ref="formRef"
            :model="formData"
            :rules="rules"
            label-position="top"
            :key="formKey"
            @keyup.enter="handleSubmit"
            class="login-form"
          >
            <ElFormItem :label="$t('login.accountLabel')" prop="username">
              <ElInput
                class="custom-height"
                :placeholder="$t('login.placeholder.username')"
                :prefix-icon="User"
                v-model.trim="formData.username"
              />
            </ElFormItem>
            <ElFormItem :label="$t('login.passwordLabel')" prop="password">
              <ElInput
                class="custom-height"
                :placeholder="$t('login.placeholder.password')"
                :prefix-icon="Lock"
                v-model.trim="formData.password"
                type="password"
                autocomplete="off"
                show-password
              />
            </ElFormItem>

            <!-- 推拽验证 -->
            <div class="relative pb-5 mt-4">
              <div
                class="relative z-[2] overflow-hidden select-none rounded-lg border border-transparent tad-300"
                :class="{ '!border-[#FF4E4F]': !isPassing && isClickPass }"
              >
                <ArtDragVerify
                  ref="dragVerify"
                  v-model:value="isPassing"
                  :text="$t('login.sliderText')"
                  textColor="#4D5875"
                  :successText="$t('login.sliderSuccessText')"
                  progressBarBg="var(--main-color)"
                  background="#E8EEF6"
                  handlerBg="#FFFFFF"
                  :height="56"
                  radius="10px"
                />
              </div>
              <p
                class="absolute top-0 z-[1] px-px mt-2 text-xs text-[#f56c6c] tad-300"
                :class="{ 'translate-y-10': !isPassing && isClickPass }"
              >
                {{ $t('login.placeholder.slider') }}
              </p>
            </div>

            <div class="flex-cb mt-2 text-sm">
              <ElCheckbox v-model="formData.rememberPassword">{{
                $t('login.rememberPwd')
              }}</ElCheckbox>
            </div>

            <div class="mt-6">
              <ElButton
                class="w-full custom-height"
                type="primary"
                @click="handleSubmit"
                :loading="loading"
                v-ripple
              >
                {{ $t('login.btnText') }}
              </ElButton>
            </div>

            <div class="admin-assigned">
              <span class="line"></span>
              <span class="text">{{ $t('login.adminAssigned') }}</span>
              <span class="line"></span>
            </div>
          </ElForm>
        </div>
      </div>
    </div>
  </div>
</template>
  • Step 2: 修改 index.vue 的 script 部分
  1. 新增 import:import { User, Lock } from '@element-plus/icons-vue'
  2. 删除:import { useSettingStore } from '@/store/modules/setting'、const settingStore = useSettingStore()、const { isDark } = storeToRefs(settingStore)(滑块已固定浅色,isDark 不再使用)
  3. 删除:type AccountKey、export interface Account、const accounts = computed... 整块、setupAccount 函数、onMounted(() => { setupAccount('admin') })
  4. formData 初始化改为 username: 'admin'(保持 M1 单管理员预填行为):
const formData = reactive({
  account: '',
  username: 'admin',
  password: '',
  rememberPassword: true
})
  1. 其余逻辑(handleSubmit、resetDragVerify、showLoginSuccessNotice、formKey/locale watch)零改动。
  • Step 3: index.vue 底部 scoped style 简化

删除 :deep(.el-select__wrapper) 覆盖块(ElSelect 已移除),@import './style.css' 保留。

  • Step 4: style.css 整文件替换
@reference '@styles/core/tailwind.css';

/* 授权页右侧区域(客迹V1:固定浅色,不随暗色主题变化) */
.auth-right-bg {
  /* 覆盖 Element Plus 暗色变量,防止 html.dark 时表单控件变暗 */
  --el-bg-color: #ffffff;
  --el-bg-color-overlay: #ffffff;
  --el-fill-color-blank: #ffffff;
  --el-fill-color-light: #f5f7fa;
  --el-text-color-primary: #1a1a1a;
  --el-text-color-regular: #303133;
  --el-border-color: #dcdfe6;
  --el-border-color-light: #e4e7ed;
  --el-border-color-lighter: #ebeef5;
  background-color: #f7f8fa;
}

.auth-right-wrap {
  @apply absolute inset-0 w-[440px] h-[680px] py-[5px] m-auto overflow-hidden
    max-sm:px-7 max-sm:w-full
    animate-[slideInRight_0.6s_cubic-bezier(0.25,0.46,0.45,0.94)_forwards]
    max-md:animate-none;

  .form {
    @apply h-full py-[40px];
  }

  .title {
    @apply text-[#1A1A1A] text-[32px] font-semibold text-center;
  }

  .sub-title {
    @apply mt-[10px] text-[#8A94A6] text-[15px] text-center;
  }

  .custom-height {
    @apply !h-[56px];
  }

  /* 表单标签样式 */
  :deep(.el-form-item__label) {
    color: #1a1a1a;
    font-weight: 600;
  }

  /* 账号由管理员分配 */
  .admin-assigned {
    @apply flex-cc gap-3 mt-8;

    .line {
      @apply inline-block w-6 h-px bg-[#D3D9E3];
    }

    .text {
      @apply text-[13px] text-[#9AA3B2];
    }
  }
}

/* 滑入动画 */
@keyframes slideInRight {
  from {
    opacity: 0;
    transform: translateX(30px);
  }

  to {
    opacity: 1;
    transform: translateX(0);
  }
}
  • Step 5: 验证无残留引用

Run: grep -rn "setupAccount\|AuthTopBar\|noAccount\|ElSelect" src/views/auth/login/ Expected: 无输出(login 页内已无这些引用;register/forget-password 中的 AuthTopBar 不受影响)

  • Step 6: Commit
git add src/views/auth/login/index.vue src/views/auth/login/style.css
git commit -m "feat(admin): 登录表单区对齐客迹V1——移除账号下拉/注册/忘记密码入口,固定浅色"

Task 4: 视觉验证(Playwright 截图对照设计稿)

Files:

  • 无代码改动;产出截图到 admin/exports/(或临时目录,不入库)

  • Step 1: 启动 dev server

Run: cd admin && pnpm dev(后台运行) Expected: Vite 启动在 http://localhost:3006

  • Step 2: Playwright 三档截图

用 playwright MCP 打开 http://localhost:3006/#/login(或路由实际形态,先 navigate 探测),分别设置视口并截图:

  1. 1920×1080(桌面标准)
  2. 1440×900(小桌面)
  3. 375×812(移动,验证左侧隐藏 + 表单单栏)
  • Step 3: 对照检查清单

  • 左侧 58vw 深蓝背景图完整覆盖、无拉伸变形

  • logo-w.png 白字清晰可见(左上角)

  • 主标语/副标题/短横线/口号/底部文案齐全、层级与留白接近设计稿

  • 右侧 #F7F8FA 底、标题居中、输入框 56px 高带图标

  • 滑块浅蓝灰底、文字 #4D5875 可读、验证通过后主色进度条

  • 登录按钮全宽主色

  • 底部"— 账号由管理员分配 —"

  • 375px:左侧隐藏、表单居中占满

  • 浏览器切 html.dark 类(Playwright evaluate document.documentElement.classList.add('dark'))后右侧仍浅色

发现问题 → 回到对应 Task 修正(样式微调不新增任务)。

  • Step 4: 实际登录冒烟

本地 dev 直连生产 API 不可用时仅验证 UI 交互:滑块可拖动、按钮可点、空值提交报错提示正常。(真实登录冒烟放到 Task 5 线上验证时用管理员账号完成,密码不入库——问用户索取或由用户自测)


Task 5: 构建验证 + 部署上线

  • Step 1: pnpm build

Run: cd admin && pnpm build Expected: 构建成功,dist/ 生成新哈希文件

  • Step 2: 标准部署流程(项目约定:测试通过后直接上线,无需等确认)
cd admin && pnpm build
tar -czf /tmp/admin-dist.tar.gz dist/
scp -i ~/.ssh/id_ed25519 -J root@8.152.213.143 /tmp/admin-dist.tar.gz root@47.254.176.10:/tmp/
ssh -i ~/.ssh/id_ed25519 -J root@8.152.213.143 root@47.254.176.10 "cd /opt/cms-cj && tar -xzf /tmp/admin-dist.tar.gz && rm /tmp/admin-dist.tar.gz"

(原地覆盖 dist 内容,bind mount 不受影响,无需重启容器)

  • Step 3: 线上验证

Playwright 打开 https://tracker.12418113.xyz 登录页,截图确认新视觉生效;记录 index-xxx.js 哈希作为版本号。

  • Step 4: 汇报

汇报内容:完成项清单、三档截图、线上版本哈希、验证结果。


风险提示(执行者注意)

  1. 严格不动服务器其他项目(1Panel、其他网站容器)——只解压覆盖 /opt/cms-cj 下的前端文件。
  2. 05-login-left-background.png 为 AI 去字重建背景,与设计稿非逐像素一致是预期内的,不要试图"修正"背景图。
  3. register/forget-password 页共用 LoginLeftView,视觉同步变化是预期行为(spec 决策 4)。
  4. 若 grep 发现 login.noAccount/login.register 有 login 页之外的引用,保留对应 i18n key 不删,并在 commit message 中说明。