Co-Authored-By: Claude Code <noreply@anthropic.com>
16 KiB
登录页客迹视觉 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 部分
- 新增 import:
import { User, Lock } from '@element-plus/icons-vue' - 删除:
import { useSettingStore } from '@/store/modules/setting'、const settingStore = useSettingStore()、const { isDark } = storeToRefs(settingStore)(滑块已固定浅色,isDark 不再使用) - 删除:
type AccountKey、export interface Account、const accounts = computed...整块、setupAccount函数、onMounted(() => { setupAccount('admin') }) formData初始化改为username: 'admin'(保持 M1 单管理员预填行为):
const formData = reactive({
account: '',
username: 'admin',
password: '',
rememberPassword: true
})
- 其余逻辑(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 探测),分别设置视口并截图:
- 1920×1080(桌面标准)
- 1440×900(小桌面)
- 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: 汇报
汇报内容:完成项清单、三档截图、线上版本哈希、验证结果。
风险提示(执行者注意)
- 严格不动服务器其他项目(1Panel、其他网站容器)——只解压覆盖 /opt/cms-cj 下的前端文件。
05-login-left-background.png为 AI 去字重建背景,与设计稿非逐像素一致是预期内的,不要试图"修正"背景图。- register/forget-password 页共用 LoginLeftView,视觉同步变化是预期行为(spec 决策 4)。
- 若 grep 发现
login.noAccount/login.register有 login 页之外的引用,保留对应 i18n key 不删,并在 commit message 中说明。