Commit 7c16c06a by DaiJiezhang

feat: migrate local frontend workflow to vite

parent e16169d0
node_modules/
dist/
.codegraph/
.codex/
.cursor/
......
# 学有为资产后台
## 本地开发
本项目的前端联调入口为 Vite;请使用两个终端分别启动后端与前端。
```powershell
# 终端 1:启动本地 Spring Boot 后端(端口 8888)
.\start-backend-jdk17.bat
# 终端 2:安装依赖并启动 Vite(端口 5173)
npm ci
npm run dev
```
开发访问地址:`http://localhost:5173/assets/#/phone-card`
支持的 Hash 路由:`overview``domain``wechat``phone-card``alerts`。页面发出的 `/api/...` 请求由 Vite 转发到本机后端 `http://localhost:8888`
## 构建与预览
```powershell
npm run build
npm run preview
```
构建产物位于 `dist/`。预览地址固定为 `http://localhost:4173/assets/#/phone-card`;该预览只用于本机检查,不代表生产部署。
## 端口与排障
- `5173`:Vite 开发服务。
- `4173`:Vite 构建预览。
- `8888`:本地 Spring Boot 后端。
- 端口已启用严格模式:若被占用,服务会直接启动失败。请先释放占用端口,再重试,不要改用其他端口。
- `start.bat``start-frontend.bat` 保留以兼容既有脚本,但不再用于前端联调;旧 Python `8000` 入口无法代理相对 `/api` 请求。
## 验收测试数据
本地受控写入验证只允许创建名称或备注以 `VITE-SMOKE-` 开头的手机号卡。请记录创建接口返回的 ID,并在验证新增、编辑和列表刷新后,仅使用同一 ID 删除该记录。
\ No newline at end of file
const DATA_SOURCES = {
const DATA_SOURCES = {
domain: [
{
id: 1,
......@@ -147,7 +147,7 @@ let phoneAppInstance = null;
window.state = state;
/**
* 代码作用(白话):路由壳层重新挂载后,刷新页面上会被旧脚本复用的 DOM 引用,避免引用指向已经被替换的节点;关联文件:F:/Project/xyw_console/src/router/index.js、F:/Project/xyw_console/index.html;关联逻辑(调用链/消息链/数据流):RouterView mounted -> refreshDOMRefs() -> renderModule()/updateActiveNav()。
* 代码作用(白话):路由壳层重新挂载后,刷新页面上会被旧脚本复用的 DOM 引用,避免引用指向已经被替换的节点;关联文件:F:/Project/xyw_console/src/router/index.js、F:/Project/xyw_console/assets/index.html;关联逻辑(调用链/消息链/数据流):RouterView mounted -> refreshDOMRefs() -> renderModule()/updateActiveNav()。
*/
function refreshDOMRefs() {
appShell = document.getElementById('appShell');
......@@ -166,7 +166,7 @@ window.refreshDOMRefs = refreshDOMRefs;
window.updateDOMRefs = refreshDOMRefs;
/**
* 代码作用(白话):对字符串做 HTML 转义,防止记录内容直接写进 innerHTML 时破坏页面结构;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):记录对象 -> escapeHtml() -> renderHeader()/renderSimpleBody()/renderDetail() -> 页面 HTML。
* 代码作用(白话):对字符串做 HTML 转义,防止记录内容直接写进 innerHTML 时破坏页面结构;关联文件:F:/Project/xyw_console/assets/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):记录对象 -> escapeHtml() -> renderHeader()/renderSimpleBody()/renderDetail() -> 页面 HTML。
*/
function escapeHtml(value) {
return String(value ?? '-')
......@@ -218,7 +218,7 @@ function showError(message) {
}
/**
* 代码作用(白话):给简单模块取对应数据源,overview 继续复用域名数据;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):renderModule() -> getModuleRecords() -> renderSimpleBody()/renderDetail()。
* 代码作用(白话):给简单模块取对应数据源,overview 继续复用域名数据;关联文件:F:/Project/xyw_console/assets/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):renderModule() -> getModuleRecords() -> renderSimpleBody()/renderDetail()。
*/
function getModuleRecords(moduleKey) {
if (moduleKey === 'overview') {
......@@ -228,7 +228,7 @@ function getModuleRecords(moduleKey) {
}
/**
* 代码作用(白话):从当前模块记录里找到已选中对象,没有命中时回退到第一条,保证简单详情面板有稳定数据;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):state.selectedBySource -> getSelectedRecord() -> renderDetail()。
* 代码作用(白话):从当前模块记录里找到已选中对象,没有命中时回退到第一条,保证简单详情面板有稳定数据;关联文件:F:/Project/xyw_console/assets/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):state.selectedBySource -> getSelectedRecord() -> renderDetail()。
*/
function getSelectedRecord(moduleKey) {
const records = getModuleRecords(moduleKey);
......@@ -241,7 +241,7 @@ function getSelectedRecord(moduleKey) {
}
/**
* 代码作用(白话):处理页面顶部壳层区域,但当前按最新界面要求不再渲染面包屑和顶部按钮,避免旧头部继续占住企微主工作区;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/src/router/index.js、F:/Project/xyw_console/styles.css;关联逻辑(调用链/消息链/数据流):renderModule() -> renderHeader() -> pageHeader.innerHTML 置空。
* 代码作用(白话):处理页面顶部壳层区域,但当前按最新界面要求不再渲染面包屑和顶部按钮,避免旧头部继续占住企微主工作区;关联文件:F:/Project/xyw_console/assets/index.html、F:/Project/xyw_console/src/router/index.js、F:/Project/xyw_console/styles.css;关联逻辑(调用链/消息链/数据流):renderModule() -> renderHeader() -> pageHeader.innerHTML 置空。
*/
function renderHeader(moduleKey) {
if (!pageHeader) {
......@@ -251,7 +251,7 @@ function renderHeader(moduleKey) {
}
/**
* 代码作用(白话):渲染简单列表模块,让 overview、domain、alerts 继续保留基础查看能力;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/styles-v2.css;关联逻辑(调用链/消息链/数据流):renderModule() -> renderSimpleBody() -> handlePageBodyClick() -> renderDetail()。
* 代码作用(白话):渲染简单列表模块,让 overview、domain、alerts 继续保留基础查看能力;关联文件:F:/Project/xyw_console/assets/index.html、F:/Project/xyw_console/styles-v2.css;关联逻辑(调用链/消息链/数据流):renderModule() -> renderSimpleBody() -> handlePageBodyClick() -> renderDetail()。
*/
function renderSimpleBody(moduleKey) {
if (!pageBody) {
......@@ -577,7 +577,7 @@ function renderExternalRuntime(moduleKey) {
}
/**
* 代码作用(白话):渲染简单模块的详情面板,复杂列表模块仍走自己的抽屉,不复用旧详情壳;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):handlePageBodyClick() -> renderDetail() -> detailDrawer 内容更新。
* 代码作用(白话):渲染简单模块的详情面板,复杂列表模块仍走自己的抽屉,不复用旧详情壳;关联文件:F:/Project/xyw_console/assets/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):handlePageBodyClick() -> renderDetail() -> detailDrawer 内容更新。
*/
function renderDetail(moduleKey) {
if (!detailDrawer || !detailShellTitle || !detailBody) {
......@@ -609,7 +609,7 @@ function renderDetail(moduleKey) {
}
/**
* 代码作用(白话):根据当前激活模块高亮左侧导航按钮;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):renderModule()/Router mounted -> updateActiveNav() -> nav-link class 切换。
* 代码作用(白话):根据当前激活模块高亮左侧导航按钮;关联文件:F:/Project/xyw_console/assets/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):renderModule()/Router mounted -> updateActiveNav() -> nav-link class 切换。
*/
function updateActiveNav() {
if (!sidebar) {
......@@ -622,7 +622,7 @@ function updateActiveNav() {
}
/**
* 代码作用(白话):按当前模块决定渲染简单列表还是复杂运行时模块,是页面切换后的总入口;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):Router mounted -> renderModule() -> renderHeader() -> renderSimpleBody()/renderExternalRuntime() -> updateActiveNav()。
* 代码作用(白话):按当前模块决定渲染简单列表还是复杂运行时模块,是页面切换后的总入口;关联文件:F:/Project/xyw_console/assets/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):Router mounted -> renderModule() -> renderHeader() -> renderSimpleBody()/renderExternalRuntime() -> updateActiveNav()。
*/
function renderModule() {
if (!state.activeModule || !pageHeader || !pageBody) {
......@@ -639,7 +639,7 @@ function renderModule() {
}
/**
* 代码作用(白话):处理简单模块列表项点击,把当前记录写进选中状态并刷新详情面板;关联文件:F:/Project/xyw_console/index.html;关联逻辑(调用链/消息链/数据流):pageBody click -> handlePageBodyClick() -> state.selectedBySource -> renderDetail()。
* 代码作用(白话):处理简单模块列表项点击,把当前记录写进选中状态并刷新详情面板;关联文件:F:/Project/xyw_console/assets/index.html;关联逻辑(调用链/消息链/数据流):pageBody click -> handlePageBodyClick() -> state.selectedBySource -> renderDetail()。
*/
function handlePageBodyClick(event) {
const button = event.target.closest('[data-record-id][data-source-key]');
......@@ -651,14 +651,14 @@ function handlePageBodyClick(event) {
}
/**
* 代码作用(白话):关闭旧详情壳层的抽屉,只影响简单模块详情显示;关联文件:F:/Project/xyw_console/index.html;关联逻辑(调用链/消息链/数据流):drawerClose click -> handleDrawerCloseClick() -> detailDrawer.hidden。
* 代码作用(白话):关闭旧详情壳层的抽屉,只影响简单模块详情显示;关联文件:F:/Project/xyw_console/assets/index.html;关联逻辑(调用链/消息链/数据流):drawerClose click -> handleDrawerCloseClick() -> detailDrawer.hidden。
*/
function handleDrawerCloseClick() {
detailDrawer?.classList.add('hidden');
}
/**
* 代码作用(白话):根据侧边栏是否折叠,同步按钮文案和可访问性属性;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/styles-v2.css;关联逻辑(调用链/消息链/数据流):handleSidebarToggleClick() -> updateSidebarToggleState() -> appShell/sidebar class + aria/text。
* 代码作用(白话):根据侧边栏是否折叠,同步按钮文案和可访问性属性;关联文件:F:/Project/xyw_console/assets/index.html、F:/Project/xyw_console/styles-v2.css;关联逻辑(调用链/消息链/数据流):handleSidebarToggleClick() -> updateSidebarToggleState() -> appShell/sidebar class + aria/text。
*/
function updateSidebarToggleState() {
if (!sidebarToggle || !appShell) {
......@@ -673,7 +673,7 @@ function updateSidebarToggleState() {
}
/**
* 代码作用(白话):切换左侧边栏折叠状态,并同步主区域布局;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/styles-v2.css;关联逻辑(调用链/消息链/数据流):sidebarToggle click -> handleSidebarToggleClick() -> sidebar/appShell class 切换 -> updateSidebarToggleState()。
* 代码作用(白话):切换左侧边栏折叠状态,并同步主区域布局;关联文件:F:/Project/xyw_console/assets/index.html、F:/Project/xyw_console/styles-v2.css;关联逻辑(调用链/消息链/数据流):sidebarToggle click -> handleSidebarToggleClick() -> sidebar/appShell class 切换 -> updateSidebarToggleState()。
*/
function handleSidebarToggleClick() {
sidebar?.classList.toggle('is-collapsed');
......@@ -682,7 +682,7 @@ function handleSidebarToggleClick() {
}
/**
* 代码作用(白话):给页面壳层重新绑定点击事件,避免 Router 重新挂载后旧事件丢失;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):initPage() -> bindLayoutEvents() -> 点击事件重新生效。
* 代码作用(白话):给页面壳层重新绑定点击事件,避免 Router 重新挂载后旧事件丢失;关联文件:F:/Project/xyw_console/assets/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):initPage() -> bindLayoutEvents() -> 点击事件重新生效。
*/
function bindLayoutEvents() {
pageBody?.removeEventListener('click', handlePageBodyClick);
......@@ -693,19 +693,24 @@ function bindLayoutEvents() {
sidebarToggle?.addEventListener('click', handleSidebarToggleClick);
}
let pageInitialized = false;
/**
* 代码作用(白话):页面初始化时刷新 DOM 引用、重绑布局事件,并在已有 activeModule 时恢复当前页面;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/src/router/index.js;关联逻辑(调用链/消息链/数据流):DOMContentLoaded -> initPage() -> refreshDOMRefs()/bindLayoutEvents() -> renderModule()。
* 代码作用(白话):在路由已挂载后只执行一次 DOM 引用刷新与布局事件绑定,并恢复当前页面渲染;关联文件:src/main.js、src/router/index.js、index.html;关联逻辑(调用链/消息链/数据流):src/main.js -> window.initPage() -> refreshDOMRefs()/bindLayoutEvents() -> renderModule()。
*/
function initPage() {
if (pageInitialized) {
return;
}
refreshDOMRefs();
bindLayoutEvents();
updateSidebarToggleState();
if (state.activeModule) {
renderModule();
}
pageInitialized = true;
}
window.updateActiveNav = updateActiveNav;
window.renderModule = renderModule;
document.addEventListener('DOMContentLoaded', initPage);
window.initPage = initPage;
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--
Resource base: resolve assets entry requests against the repository root so existing styles, images, scripts, and dependencies remain available.
Related files: F:/Project/xyw_console/assets/index.html and F:/Project/xyw_console/start-frontend.bat.
Flow: /assets/#/page -> base resolution -> root static resources -> Vue Router page render.
-->
<base href="../" />
<title>学有为资产管理台</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230f766e'/%3E%3Ctext x='50%25' y='55%25' text-anchor='middle' font-size='26' font-family='Arial' fill='white'%3EXY%3C/text%3E%3C/svg%3E" />
<link rel="stylesheet" href="node_modules/element-plus/dist/index.css" />
<link rel="stylesheet" href="styles-v2.css?v=20260720-fix-list" />
</head>
<body>
<div class="workspace" id="appShell">
<aside class="shell-nav" id="sidebar">
<div class="brand-panel">
<div class="brand-mark">
<img class="brand-logo" src="img/学有为logo.jpg" alt="学有为 LOGO" />
</div>
<div class="brand-copy">
<h1>学有为数据资产后台</h1>
</div>
</div>
<div class="nav-main">
<nav class="primary-nav" aria-label="主导航">
<button class="nav-link" type="button" data-module="overview">
<span class="nav-glyph"><img class="nav-icon" src="img/账户总览.png" alt="总览" /></span>
<span class="nav-label">总览</span>
</button>
<button class="nav-link" type="button" data-module="domain">
<span class="nav-glyph"><img class="nav-icon" src="img/域名申请.png" alt="域名资料" /></span>
<span class="nav-label">域名资料</span>
</button>
<button class="nav-link" type="button" data-module="wechat">
<span class="nav-glyph"><img class="nav-icon" src="img/企微_企微.png" alt="企微资料" /></span>
<span class="nav-label">企微资料</span>
</button>
<button class="nav-link" type="button" data-module="phone">
<span class="nav-glyph"><img class="nav-icon" src="img/电话号码 (1).png" alt="手机号卡" /></span>
<span class="nav-label">手机号卡</span>
</button>
<button class="nav-link" type="button" data-module="alerts">
<span class="nav-glyph"><img class="nav-icon" src="img/提醒中心.png" alt="提醒中心" /></span>
<span class="nav-label">提醒中心</span>
</button>
</nav>
</div>
<div class="nav-bottom">
<div class="nav-toggle-wrap">
<button class="nav-toggle-button" id="sidebarToggle" type="button" aria-label="收起侧边导航" aria-expanded="true">
<span class="nav-toggle-glyph" aria-hidden="true">
<span></span>
<span></span>
</span>
<span class="nav-toggle-label">收起</span>
</button>
</div>
<div class="nav-footer">
<div class="nav-user-avatar"></div>
<div class="nav-user-copy">
<p class="nav-user-name">管理员</p>
<p class="nav-user-role">超级管理员</p>
</div>
<button class="nav-settings" type="button" aria-label="设置"></button>
</div>
</div>
</aside>
<main class="shell-main" id="vue-router-app">
<router-view></router-view>
</main>
</div>
<div id="addDomainDialogRoot"></div>
<div id="wechatDialogRoot"></div>
<div id="wechatDetailDrawerRoot"></div>
<div id="phoneDialogRoot"></div>
<div id="phoneDetailDrawerRoot"></div>
<script src="node_modules/vue/dist/vue.global.prod.js"></script>
<script src="node_modules/vue-router/dist/vue-router.global.prod.js"></script>
<script src="node_modules/element-plus/dist/index.full.min.js"></script>
<script src="src/modules/domain/domain-add-dialog.js"></script>
<script src="src/modules/domain/domain-list-runtime.js"></script>
<script src="src/modules/overview/overview-list-runtime.js"></script>
<script src="src/modules/wechat/wechat-add-dialog.js"></script>
<script src="src/modules/wechat/wechat-detail-drawer.js"></script>
<script src="src/modules/wechat/wechat-list-runtime.js"></script>
<script src="src/modules/phone/phone-add-dialog.js"></script>
<script src="src/modules/phone/phone-detail-drawer.js"></script>
<script src="src/modules/phone/phone-image-cell.js?v=20260707"></script>
<script src="src/modules/phone/phone-list-runtime.js?v=20260707"></script>
<script src="src/modules/shared/record-adapters.js"></script>
<script src="src/modules/shared/wechat-api-client.js"></script>
<script src="src/modules/shared/phone-api-client.js"></script>
<script src="src/router/index.js"></script>
<script src="app-v2.js"></script>
</body>
</html>
......@@ -6,16 +6,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>学有为资产管理台</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%230f766e'/%3E%3Ctext x='50%25' y='55%25' text-anchor='middle' font-size='26' font-family='Arial' fill='white'%3EXY%3C/text%3E%3C/svg%3E" />
<link rel="stylesheet" href="node_modules/element-plus/dist/index.css" />
<link rel="stylesheet" href="styles-v2.css?v=20260720-fix-list" />
</head>
</head>
<body>
<div class="workspace" id="appShell">
<aside class="shell-nav" id="sidebar">
<div class="brand-panel">
<div class="brand-mark">
<img class="brand-logo" src="img/学有为logo.jpg" alt="学有为 LOGO" />
<img class="brand-logo" src="/img/学有为logo.jpg" alt="学有为 LOGO" />
</div>
<div class="brand-copy">
<h1>学有为数据资产后台</h1>
......@@ -25,23 +23,23 @@
<div class="nav-main">
<nav class="primary-nav" aria-label="主导航">
<button class="nav-link" type="button" data-module="overview">
<span class="nav-glyph"><img class="nav-icon" src="img/账户总览.png" alt="总览" /></span>
<span class="nav-glyph"><img class="nav-icon" src="/img/账户总览.png" alt="总览" /></span>
<span class="nav-label">总览</span>
</button>
<button class="nav-link" type="button" data-module="domain">
<span class="nav-glyph"><img class="nav-icon" src="img/域名申请.png" alt="域名资料" /></span>
<span class="nav-glyph"><img class="nav-icon" src="/img/域名申请.png" alt="域名资料" /></span>
<span class="nav-label">域名资料</span>
</button>
<button class="nav-link" type="button" data-module="wechat">
<span class="nav-glyph"><img class="nav-icon" src="img/企微_企微.png" alt="企微资料" /></span>
<span class="nav-glyph"><img class="nav-icon" src="/img/企微_企微.png" alt="企微资料" /></span>
<span class="nav-label">企微资料</span>
</button>
<button class="nav-link" type="button" data-module="phone">
<span class="nav-glyph"><img class="nav-icon" src="img/电话号码 (1).png" alt="手机号卡" /></span>
<span class="nav-glyph"><img class="nav-icon" src="/img/电话号码 (1).png" alt="手机号卡" /></span>
<span class="nav-label">手机号卡</span>
</button>
<button class="nav-link" type="button" data-module="alerts">
<span class="nav-glyph"><img class="nav-icon" src="img/提醒中心.png" alt="提醒中心" /></span>
<span class="nav-glyph"><img class="nav-icon" src="/img/提醒中心.png" alt="提醒中心" /></span>
<span class="nav-label">提醒中心</span>
</button>
</nav>
......@@ -80,24 +78,7 @@
<div id="phoneDialogRoot"></div>
<div id="phoneDetailDrawerRoot"></div>
<script src="node_modules/vue/dist/vue.global.prod.js"></script>
<script src="node_modules/vue-router/dist/vue-router.global.prod.js"></script>
<script src="node_modules/element-plus/dist/index.full.min.js"></script>
<script src="src/modules/domain/domain-add-dialog.js"></script>
<script src="src/modules/domain/domain-list-runtime.js"></script>
<script src="src/modules/overview/overview-list-runtime.js"></script>
<script src="src/modules/wechat/wechat-add-dialog.js"></script>
<script src="src/modules/wechat/wechat-detail-drawer.js"></script>
<script src="src/modules/wechat/wechat-list-runtime.js"></script>
<script src="src/modules/phone/phone-add-dialog.js"></script>
<script src="src/modules/phone/phone-detail-drawer.js"></script>
<script src="src/modules/phone/phone-image-cell.js?v=20260707"></script>
<script src="src/modules/phone/phone-list-runtime.js?v=20260707"></script>
<script src="src/modules/shared/record-adapters.js"></script>
<script src="src/modules/shared/wechat-api-client.js"></script>
<script src="src/modules/shared/phone-api-client.js"></script>
<script src="src/router/index.js"></script>
<script src="app-v2.js"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>
schema: spec-driven
created: 2026-07-25
# migrate-vite-build
将资产后台迁移为本地 Vite 构建与 API 代理
## Context
资产后台现有页面使用 Hash 路由和依赖 `window.*` 全局对象的旧模块。页面当前由 Python 临时静态服务托管,并直接读取 `node_modules`;后端本地端口为 `8888`。工作区已有未提交改动和未跟踪的 `assets/index.html`,迁移必须以它们为基线增量进行。
## Goals / Non-Goals
**Goals:**
- 以 Vite 提供稳定的本地开发、构建和预览流程。
- 固定开发入口为 `http://localhost:5173/assets/#/...`,预览入口为 `http://localhost:4173/assets/#/...`
- 使 `/api/...` 经 Vite 转发到本机 `8888`,不改变后端接口契约。
- 保持五个 Hash 路由与旧页面模块的加载、路由和初始化行为。
**Non-Goals:**
- 不部署生产环境,不调整域名、HTTPS、服务器根路径或生产代理。
- 不修改后端 Controller、DTO、数据库、业务规则或启动批处理脚本。
- 不删除或修改 `assets/index.html`,也不承诺旧 Python `8000` 前端入口可继续联调。
## Decisions
### 固定 Vite 地址与资源前缀
Vite 使用 `base: '/assets/'`,开发端口固定为 `5173`、预览端口固定为 `4173`,并启用严格端口。固定地址可让 README、验证和未来构建资源路径一致;不选择自动换端口,避免服务实际地址与预期不一致。
### 以相对 `/api` 访问本地后端
两个 API 客户端仅把根地址调整为 `/api/wechat-records``/api/wx-phones`。Vite 仅代理 `/api/``http://localhost:8888`。相对路径避免把本机主机地址写入前端构建产物,也避免浏览器跨域;不改动 HTTP 方法、请求体、响应解析或字段。
### 显式且顺序化地启动旧模块
`src/main.js` 先导入 Vue、Vue Router 与 Element Plus 并写入 `window`,再依既定顺序动态导入旧模块、检查每项预期全局对象,最后显式调用路由和页面初始化。旧模块不再依赖 `DOMContentLoaded` 自动启动;异步导入结束后才启动能避免漏启动。路由与页面初始化加入幂等保护,重复调用只执行一次。
### 保留旧入口参考与现有工作区改动
`index.html` 改为 Vite 入口,页面壳参考 `assets/index.html`,但后者保持原样。迁移不还原现有改动,也不把 `.idea/``start.bat` 纳入本次改动范围。
## Risks / Trade-offs
- [框架全局对象晚于旧模块加载而白屏] → 先写入框架全局对象,按顺序导入并逐项检查缺失对象。
- [异步加载造成路由或页面未启动] → 删除旧的自动启动依赖,全部模块就绪后显式初始化。
- [重复初始化造成重复请求或事件] → 路由与页面入口设置幂等保护,并通过刷新及路由切换验证。
- [端口占用造成联调误判] → 严格端口直接失败;README 说明释放占用后重试。
- [受控写入测试数据遗留] → 仅使用 `VITE-SMOKE-` 标识创建数据,记录返回 ID 并按同一 ID 清理。
## Migration Plan
1. 记录当前 Git 差异和未跟踪 `assets/` 基线。
2. 添加 Vite 依赖、命令、配置和构建产物忽略规则。
3. 迁移入口页面与所需公共图片,添加兼容启动入口。
4. 改造路由、页面初始化和两个 API 根地址。
5. 添加 README,并执行构建、预览、开发联调与受控手机号卡写入验证。
若某一组变更验证失败,仅恢复该组的增量改动并保留先前工作区基线;不使用 `git reset --hard`。生产部署与生产回滚不属于本次迁移,未来由独立提案处理。
## Open Questions
- 无。开发、预览、后端端口和 `/assets/` 前缀均已确认。
## Why
资产后台目前依赖 Python 临时静态服务和浏览器直接读取 `node_modules`,本地启动、资源路径与接口联调都缺少稳定且可重复的工程化入口。现在迁移到 Vite,可在不改变后端业务、数据库、DTO 或生产环境的前提下,建立固定的本地开发、构建和预览流程。
## What Changes
- 增加以 `/assets/` 为固定前缀、端口为 `5173` 的 Vite 本地开发入口,并把 `/api/...` 请求代理到本机 Spring Boot `8888`
- 增加端口为 `4173` 的本机构建预览,并提供可重复执行的 `dev``build``preview` 命令。
- 将前端 API 根地址改为相对 `/api/...`,保留 HTTP 方法、请求体、响应解析、字段名和错误文案。
- 使既有五个 Hash 路由、Vue、Vue Router、Element Plus、样式、图片和旧 Runtime 在 Vite 中可用,并确保页面初始化不重复执行。
- 保留 `assets/index.html` 与现有启动批处理文件;旧 Python 前端入口不再作为联调入口。生产部署、服务器配置与生产环境均不在本次范围内。
## Capabilities
### New Capabilities
- `local-vite-development`: 资产后台在本机通过固定 Vite 地址开发、构建、预览,并经 Vite 代理访问本机后端的能力。
### Modified Capabilities
- 无。项目当前没有 OpenSpec 基线规格;本次不修改既有规格。
## Impact
- 前端:`package.json`、锁文件、根入口、Vite 配置、启动入口、路由、页面初始化、两个 API 客户端、样式和公共图片。
- 文档:新增本地启动、端口、构建、预览与旧前端脚本弃用说明。
- 依赖:新增 Vite 7.x 开发依赖。
- 兼容边界:不修改后端 Controller、接口契约、DTO、数据库、生产部署、域名、HTTPS 或线上数据。
## ADDED Requirements
### Requirement: 固定本地 Vite 开发入口
系统 SHALL 通过 Vite 在端口 `5173` 提供资产后台,且页面入口 SHALL 使用 `/assets/` 前缀。端口被占用时,Vite MUST 启动失败而不得自动更换端口。
#### Scenario: 开发地址可访问
- **WHEN** 开发者执行 `npm run dev`
- **THEN** 页面可在 `http://localhost:5173/assets/#/phone-card` 访问
#### Scenario: 开发端口被占用
- **WHEN** 端口 `5173` 已被其他进程占用且开发者执行 `npm run dev`
- **THEN** Vite 启动失败并报告端口冲突
### Requirement: 相对 API 请求经本地代理转发
前端对企微与手机号卡的 API 请求 SHALL 使用相对 `/api/...` 路径,Vite SHALL 将该路径转发到本机 `http://localhost:8888`。迁移 MUST 不改变请求方法、请求体、响应解析、字段名或后端接口契约。
#### Scenario: 读取手机号卡数据
- **WHEN** 用户在 Vite 开发入口访问手机号卡页面并触发读取
- **THEN** 浏览器请求路径为 `/api/wx-phones`,且请求经 Vite 转发至本机后端并返回现有格式的数据
#### Scenario: 保存手机号卡数据
- **WHEN** 用户在 Vite 开发入口保存手机号卡
- **THEN** 请求使用相对 `/api/...` 路径并保持迁移前的 HTTP 方法、请求体和响应处理逻辑
### Requirement: 旧模块以完整依赖顺序启动一次
系统 SHALL 在加载旧模块前提供 Vue、Vue Router 和 Element Plus 的预期 `window` 全局对象,并 SHALL 按既定顺序导入旧模块。任一预期全局对象缺失时,系统 MUST 显示来源文件与缺失对象并停止启动。路由和页面初始化重复调用时 MUST 只完成一次真实初始化。
#### Scenario: 全部模块可用时启动
- **WHEN** 所有旧模块导入完成且其预期全局对象均存在
- **THEN** 系统依次初始化 Hash 路由和页面,且不出现重复挂载、重复点击或重复请求
#### Scenario: 某模块缺少预期对象
- **WHEN** 一个已导入模块未提供其要求的 `window` 全局对象
- **THEN** 系统停止页面启动并显示该模块来源文件和缺失对象名称
### Requirement: Hash 路由与资源在开发和预览中可用
系统 SHALL 支持 `overview``domain``wechat``phone-card``alerts` 五个 Hash 路由,并在刷新后保持当前路由。开发构建与预览 MUST 不请求 `node_modules/`、旧 Python 静态资源或源码脚本,且所需样式、图片和框架资源不得发生 404。
#### Scenario: 切换并刷新 Hash 路由
- **WHEN** 用户切换到任一受支持的 Hash 路由后刷新浏览器
- **THEN** 页面保持在同一路由并正常渲染对应页面
#### Scenario: 构建预览页面
- **WHEN** 开发者执行 `npm run build` 后执行 `npm run preview`
- **THEN** 页面可在 `http://localhost:4173/assets/#/phone-card` 访问,且 Network 中没有受管资源 404 或 `node_modules/` 请求
### Requirement: 受控手机号卡写入验证可清理
系统 SHALL 支持以 `VITE-SMOKE-` 开头的手机号卡测试记录完成新增、编辑和按返回 ID 删除。验证流程 MUST 只清理本轮创建并已记录 ID 的测试记录。
#### Scenario: 创建、编辑并清理测试记录
- **WHEN** 验证人员创建一条带有 `VITE-SMOKE-` 标识的手机号卡并记录返回 ID
- **THEN** 该记录可编辑并在列表刷新后可见,最后使用同一 ID 删除
## 1. 工作区基线与依赖
- [x] 1.1 记录 Git 差异和未跟踪 `assets/` 基线,确保迁移不覆盖现有用户改动。
- [x] 1.2 在 `package.json` 增加 Vite 7.x、`dev``build``preview` 命令和 Node 版本要求,并更新锁文件。
- [x] 1.3 在 `.gitignore` 忽略 `dist/`,执行 `npm ci` 验证锁文件可重复安装。
## 2. Vite 入口与静态资源
- [x] 2.1 新增 `vite.config.js`,配置 `/assets/` 前缀、开发 `5173`、预览 `4173`、严格端口和 `/api` 到本机 `8888` 的代理。
- [x] 2.2 将页面壳迁入根 `index.html`,只加载 Vite 入口,并保留 `assets/index.html` 不修改。
- [x] 2.3 将页面实际使用的 Logo 和五个导航图标复制到 `public/img/`,保持文件名并验证资源路径。
## 3. 兼容启动与 API 迁移
- [x] 3.1 新增 `src/main.js`,导入框架并写入所需 `window` 全局对象,按既定顺序加载旧模块、检查全局能力并显式启动页面。
- [x] 3.2 为本次新增或修改的每个方法、对象方法、箭头函数和业务回调添加新手注释,写明代码作用、关联文件和调用链或数据流。
- [x] 3.3 改造 `src/router/index.js``app-v2.js`,移除 `DOMContentLoaded` 自动启动依赖并加入一次性初始化保护。
- [x] 3.4 仅将企微和手机号卡 API 客户端根地址改为相对 `/api/...`,保留其余请求与响应逻辑。
## 4. 文档与自动验证
- [x] 4.1 新增 README 本地启动说明,明确 `5173``4173``8888``/api` 代理及旧 Python 前端入口弃用状态。
- [x] 4.2 执行 `npm run build`,确认 `dist/` 成功生成。
- [x] 4.3 执行 `npm run preview`,检查 `4173/assets/` 的路由、样式、图片和框架资源无 404。
## 5. 本地联调与清理
- [x] 5.1 启动本地后端和 Vite 开发服务,验证五个 Hash 路由可切换并在刷新后保持。
- [x] 5.2 检查 Console 与 Network,确认无缺失全局对象、重复挂载、旧静态资源或 `node_modules/` 请求,且 API 使用 `/api/...`
- [ ] 5.3 创建、编辑并按返回 ID 删除一条 `VITE-SMOKE-` 手机号卡;若清理失败,记录 ID、原因和人工清理步骤。
## Context
本变更将手机号卡套餐字段改为保号套餐,并建立“账号来源表维护、手机号卡展示同步结果”的模型。`wx_phone` 位于手机号独立数据源;企业微信资料 `wx_data` 位于主数据源,已包含 `phone_number``account`,可直接作为企业微信来源。
本期只新建抖音和微信来源表。企业微信复用 `wx_data`,不创建 `linked_wecom_account`,也不复制其维护逻辑。
## Goals / Non-Goals
### Goals
- `numberProtectionPackage` / `number_protection_package` 成为唯一套餐字段。
- `wechat` 成为微信账号文本,不再表示开通状态。
- 三类账号通过专用同步接口写入 `wx_phone`,普通保存无法修改。
- 同步支持选择记录、筛选结果和全部记录三种范围。
- 来源缺失或为空时保留手机号卡原账号。
### Non-Goals
- 不兼容 `packageChange5yuan`、旧 `wechat` 布尔值或旧数据。
- 不开发两张新来源表的维护页面和 CRUD 接口。
- 不创建企业微信来源表,不修改 `wx_data` 的现有维护接口。
- 不实现权限、审计、定时任务或外部平台拉取。
## Data Model
| 位置 | 列 | 类型 | 含义 |
|---|---|---|---|
| `wx_phone` | `number_protection_package` | `TINYINT(1) NOT NULL DEFAULT 0` | 保号套餐;`0` 未办理,`1` 已办理。 |
| `wx_phone` | `douyin_account` | `TEXT NULL` | 抖音账号同步展示结果。 |
| `wx_phone` | `wechat` | `TEXT NULL` | 微信账号同步展示结果。 |
| `wx_phone` | `linked_wecom` | `TEXT NULL` | 企业微信账号同步展示结果。 |
| 抖音来源 | `douyin_account` | `phone_number``douyin_account` | 一条记录存一个抖音账号。 |
| 微信来源 | `wechat_account` | `phone_number``wechat` | 一条记录存一个微信账号。 |
| 企业微信来源 | 现有 `wx_data` | `phone_number``account` | 一条记录存一个企业微信账号。 |
同一个手机号可在每张来源表中有多行;手机号卡不逐行复制,而是把同类账号合并成逗号文本用于展示。
## Sync Flow
```mermaid
flowchart TD
A[管理员选择同步范围并确认] --> B[前端计算明确的手机号卡 ID 列表]
B --> C[POST /api/wx-phones/account-sync]
C --> D[逐张读取 wx_phone 手机号]
D --> E[查询 douyin_account]
E --> F[查询 wechat_account]
F --> G[通过 WxDataMapper 查询 wx_data.account]
G --> H{三类来源查询都成功?}
H -->|否| I[该卡标记失败且不写入]
H -->|是| J[去空格、忽略空值、去重并按 id 排序拼接]
J --> K{任一来源有有效账号?}
K -->|否| L[保留原账号并标记未更新]
K -->|是| M[一次更新 wx_phone 三个结果字段]
I --> N[处理下一张卡]
L --> N
M --> N
N --> O[返回成功、未更新、失败汇总]
O --> P[前端提示并刷新列表]
```
## API Design
普通 `POST /api/wx-phones``PUT /api/wx-phones/{id}` 只接受基础资料和 `numberProtectionPackage``douyinAccount``wechat``linkedWecom``packageChange5yuan` 均不是合法入参,传入时返回 400。
新增 `POST /api/wx-phones/account-sync`
```json
{ "phoneIds": [101, 102] }
```
接口不支持缺少 `phoneIds` 时的隐式全量同步。前端必须显式计算并发送 ID,避免直接调用接口误同步所有手机号卡。
## Selection Rules
1. 有勾选:使用勾选 ID。
2. 无勾选且当前存在筛选:使用当前筛选结果的全部 ID。
3. 无勾选且无筛选:使用当前全量列表的全部 ID。
4. 同步前显示本次数量并要求确认;目标列表为空时不请求接口。
## Error Handling
- 单张卡来源查询失败:返回 `FAILED`,不写入该卡;批量继续。
- 三类来源均无有效值:返回 `UNCHANGED`,保留三个原值。
- 有一类或多类来源有有效值:仅覆盖有有效值的字段,其他字段保留原值;一次更新该手机号卡。
- 同步接口响应包含 `requestedCount``updatedCount``unchangedCount``failedCount` 和逐卡结果。
## Migration and Rollback
迁移由人工执行的预览库 SQL 完成。应用启动只校验最终结构;任何最终列或来源表缺失时停止启动并显示明确错误,不自动修改数据库。
回滚依赖迁移前 `wx_phone` 备份。删除旧 `wechat` 布尔列不能从新值自动还原,因此不提供猜测式回滚 SQL。
## Implementation Annotation Gate
所有实现方法必须以中文注释说明代码作用、关联文件和数据流,尤其是跨数据源读取、结果合并、空来源保留、失败不写入、目标 ID 计算与测试断言。
\ No newline at end of file
## Why
手机号卡的“可变更 5 元套餐”不能表达真实的保号业务;账号信息也不应由普通手机号编辑流程改写。项目仍是预览环境,本次直接采用最终字段,不保留旧套餐字段、旧微信开通状态或旧数据兼容逻辑。
企业微信账号已有可复用来源:主数据源中的 `wx_data` 表。该表用 `phone_number` 关联手机号,用 `account` 保存企业微信账号。因此本次只新建抖音和微信账号来源表,不新建企业微信账号表。
## What Changes
- **BREAKING**`wx_phone.package_change_5yuan` 直接改为 `number_protection_package`;前端和 API 统一使用 `numberProtectionPackage`,不再接受或返回 `packageChange5yuan`
- **BREAKING** 删除 `wx_phone.wechat` 的“是否开通微信”布尔含义,改为可空文本字段,保存同步后的微信账号。
- `wx_phone.douyin_account``wx_phone.wechat``wx_phone.linked_wecom` 成为只读同步结果字段;普通手机号新增和编辑接口不得接收或写入这三个字段。
- 新建 `douyin_account``wechat_account` 两张账号来源表;每条账号一行,以 `phone_number` 关联手机号。
- 复用 `wx_data.phone_number``wx_data.account` 作为企业微信账号来源;不得创建 `linked_wecom_account` 表。
- 新增手动同步接口和列表按钮。同步分别读取三类来源,同一手机号的多个账号去空格、去重、按来源记录 `id` 升序用英文逗号拼接,再写入 `wx_phone`
- 来源记录不存在或账号值为空时保留 `wx_phone` 原值;单张手机号卡的三类结果全部处理完成后才写入,查询失败时该卡不更新。
- 支持勾选同步、筛选结果同步和全量同步:勾选时同步勾选记录;未勾选且有筛选时同步当前筛选结果;未勾选且无筛选时同步整个手机号列表。
## Capabilities
### New Capabilities
- `phone-protection-plan`: 手机号卡保号套餐字段和直接字段迁移。
- `phone-account-sync-fields`: 三类账号来源查询、只读结果字段与手动同步。
### Modified Capabilities
- None. 当前 `openspec/specs/` 中没有可修改的既有能力规格。
## Impact
- 前端:`phone-add-dialog.js``phone-list-runtime.js``phone-detail-drawer.js``record-adapters.js``phone-api-client.js``app-v2.js`
- 后端:手机号实体、普通保存 DTO/服务、同步 DTO/服务、`WxPhoneController`,以及现有 `WxDataMapper`
- 数据库:修改 `wx_phone` 三类字段和套餐列;创建 `douyin_account``wechat_account`;为三张来源表的 `phone_number` 建立或确认索引。复用 `wx_data`,不创建企业微信来源表。
- API:普通 `POST``PUT /api/wx-phones` 的字段缩减;新增 `POST /api/wx-phones/account-sync`
## Context
`wx_phone` 位于手机号独立数据源;现有 `wx_data` 位于主数据源。同步服务跨两个数据源读取企业微信、抖音和微信来源,最终只向 `wx_phone` 写入一次结果。企业微信来源不是手机号数据源中的表,不能在手机号来源 Mapper 中假定同库查询。
已确认 `wx_data.phone_number` 对应手机号,`wx_data.account` 对应企业微信账号。多个 `wx_data` 记录命中同一手机号时,所有非空 `account` 都参与合并。
## Goals / Non-Goals
**Goals:**
- 统一保号套餐的页面、API、实体和数据库语义。
-`wechat` 表示微信账号文本,而不是开通状态。
- 让三类账号只由专用同步入口更新。
- 复用 `wx_data` 作为企业微信来源,避免重复建表和重复维护数据。
- 支持单条、勾选、筛选结果和全量手机号同步。
**Non-Goals:**
- 不开发抖音、微信来源表的维护页面或 CRUD 接口。
- 不修改 `wx_data` 的现有企业微信资料维护流程。
- 不实现登录、权限、操作人审计、定时同步或外部平台调用。
- 不兼容旧套餐字段、旧微信开通状态或旧数据库数据。
## Decisions
### 1. 预览环境直接采用最终字段
`package_change_5yuan` 直接改为 `number_protection_package`;旧 `wechat` 布尔列删除后新建同名文本列;旧字段不再被 DTO、响应或前端适配器识别。
### 2. 企业微信复用 `wx_data`
企业微信来源固定为 `wx_data.account`,关联键固定为 `wx_data.phone_number`。同步服务通过 `WxDataMapper` 查询来源数据,不创建 `linked_wecom_account`
### 3. 一条账号一行,手机号卡只保存展示结果
`douyin_account``wechat_account``wx_data` 允许同一手机号有多行。同步按来源 `id` 升序读取,去掉首尾空格、忽略空值、去重,再用英文逗号拼接,写入 `wx_phone` 三个结果字段。
### 4. 同步以单张手机号卡为最小保护范围
同步服务先完成三类来源查询和合并;全部查询成功后才一次更新该手机号卡的三个结果字段。任一来源查询报错时,该卡标记失败且不写入;批量中的其他卡继续处理。这是“先查全、后一次写入”的保护方式,不要求两个数据源使用联合事务。
### 5. 空来源保留已有结果
某类来源没有该手机号,或查询到的账号均为空时,不清空 `wx_phone` 对应字段;只有查询到至少一个有效账号时才覆盖。
## Risks / Trade-offs
- [删除旧 `wechat` 布尔列] → 迁移前备份预览表结构与数据;回滚使用备份恢复,不能从新文本字段推回旧布尔值。
- [跨数据源读取] → 所有来源读取成功后再进行单次手机号卡写入;读取异常时该卡不写入。
- [批量同步范围过大] → 前端确认弹窗显示精确手机号卡数量;接口始终接收明确 `phoneIds`,不由后端猜测“全部”范围。
- [来源表数据增加后查询变慢] → 为 `douyin_account.phone_number``wechat_account.phone_number``wx_data.phone_number` 建立或确认索引。
## Migration Plan
1. 备份 `wx_phone` 的结构与数据,并保存 `SHOW CREATE TABLE wx_phone` 输出。
2. 确认 `wx_data` 数据;仅为缺失的 `wx_data.phone_number` 索引补建索引,不修改其账号数据。
3. 在预览库一次性执行迁移:改名套餐列,删除旧 `wechat` 布尔列,新增文本 `wechat`,调整两个已有账号结果列,并创建 `douyin_account``wechat_account`
4. 部署最终字段代码;启动时只检查最终表结构,不自动执行 `ALTER TABLE`
5. 验证普通保存、单条同步、筛选同步、全量同步和失败保护。
回滚使用迁移前 `wx_phone` 备份恢复结构和数据;由于旧微信布尔值已删除,不能只靠改列名回滚。
## Open Questions
- 本期不处理来源账号维护界面和权限;新增登录与权限管理时,需定义三张来源数据的维护权限和手动同步权限。
## Implementation Annotation Gate
实施时,所有新增或修改的方法必须有中文新手注释,写明:代码作用、关联文件、关联调用链或数据流。重点覆盖来源账号查询、账号合并、空来源保留、单卡失败保护、同步目标计算、确认提示、列表刷新和测试断言。
\ No newline at end of file
## ADDED Requirements
### Requirement: 三类账号字段必须从普通编辑流程移除
系统 SHALL 将 `douyin_account``wechat``linked_wecom` 视为同步结果字段。手机号新增/编辑弹窗不得展示这三个输入项,手机号列表不得允许行内编辑,普通保存接口不得接收或写入对应字段。
#### Scenario: 打开新增或编辑手机号卡弹窗
- **WHEN** 用户打开新增或编辑手机号卡弹窗
- **THEN** 页面 MUST 不显示抖音账号、微信账号、企业微信账号输入框或“开通微信”开关
#### Scenario: 普通保存请求试图修改账号
- **WHEN** 客户端向普通新增或编辑接口提交 `douyinAccount``wechat``linkedWecom`
- **THEN** 系统 MUST 返回 400,且不得修改三个同步结果字段
### Requirement: 企业微信来源必须复用现有 wx_data 表
系统 SHALL 使用 `wx_data.phone_number` 关联手机号,使用 `wx_data.account` 作为企业微信账号来源。系统 MUST 不创建 `linked_wecom_account` 表。
#### Scenario: 同一手机号存在多个企业微信来源记录
- **WHEN** `wx_data` 中存在多条相同 `phone_number``account` 非空的记录
- **THEN** 系统 MUST 按记录 `id` 升序读取、去空格和去重,并以英文逗号拼接后作为企业微信同步结果
### Requirement: 手动同步必须汇总三类账号来源
系统 SHALL 从 `douyin_account``wechat_account``wx_data` 分别读取抖音、微信、企业微信来源账号,并将结果写入 `wx_phone.douyin_account``wx_phone.wechat``wx_phone.linked_wecom`
#### Scenario: 同一手机号有多个抖音账号
- **WHEN** `douyin_account` 中同一 `phone_number` 对应多条非空账号记录
- **THEN** 系统 MUST 按记录 `id` 升序去空格和去重,并以英文逗号拼接后写入 `wx_phone.douyin_account`
#### Scenario: 来源不存在或账号为空
- **WHEN** 某类来源没有该手机号,或该手机号的账号值均为空
- **THEN** 系统 MUST 保留 `wx_phone` 中该类账号原值,不得清空
#### Scenario: 单张手机号卡的来源查询失败
- **WHEN** 同步某张手机号卡时任一来源查询失败
- **THEN** 系统 MUST 不更新该手机号卡的任一账号结果字段,并将该卡标记为失败
### Requirement: 手动同步范围必须由前端显式确定
系统 SHALL 只接受带有明确 `phoneIds` 的同步请求。前端 SHALL 根据勾选和筛选状态计算该数组,并在同步前向用户显示同步数量。
#### Scenario: 用户勾选手机号卡
- **WHEN** 用户勾选一条或多条手机号卡后点击同步
- **THEN** 前端 MUST 只提交勾选记录的 ID
#### Scenario: 用户未勾选但存在筛选条件
- **WHEN** 用户未勾选手机号卡、当前列表存在筛选条件并点击同步
- **THEN** 前端 MUST 提交当前筛选结果的全部 ID
#### Scenario: 用户未勾选且不存在筛选条件
- **WHEN** 用户未勾选手机号卡、当前列表不存在筛选条件并点击同步
- **THEN** 前端 MUST 提交当前手机号列表的全部 ID
\ No newline at end of file
## ADDED Requirements
### Requirement: 保号套餐字段必须成为唯一套餐字段
系统 SHALL 在手机号卡新增和编辑界面显示“保号套餐”开关,使用 `numberProtectionPackage` 作为前端、保存接口和实体字段,使用 `number_protection_package` 作为 `wx_phone` 数据库列。系统 SHALL 不再接受或返回 `packageChange5yuan`
#### Scenario: 新增手机号卡时办理保号套餐
- **WHEN** 用户在新增手机号卡弹窗中将“保号套餐”设为已办理并提交
- **THEN** 系统 MUST 将 `numberProtectionPackage=true` 保存到 `wx_phone.number_protection_package`
#### Scenario: 普通保存请求携带旧套餐字段
- **WHEN** 客户端向普通新增或编辑接口提交 `packageChange5yuan`
- **THEN** 系统 MUST 返回 400,且不得修改手机号卡记录
### Requirement: 微信字段必须表示微信账号文本
系统 SHALL 将 `wx_phone.wechat` 用作可空微信账号文本,不得再将其解释为“开通微信”的真假状态。
#### Scenario: 查看已有手机号卡
- **WHEN** 客户端查看手机号卡列表或详情
- **THEN** 系统 MUST 返回 `wechat` 的文本账号值或空值,页面 MUST 不显示“已开通 / 未开通”微信状态
### Requirement: 预览库结构必须由人工迁移并在启动时校验
系统 MUST 使用一次性人工迁移将套餐列改名、将旧 `wechat` 布尔列替换为文本列,并创建最终所需来源表。应用启动时 MUST 只校验最终结构,不得自动执行 `ALTER TABLE`
#### Scenario: 最终结构完整时启动
- **WHEN** `wx_phone` 最终字段和所需来源表均存在
- **THEN** 应用 MUST 正常启动且不得修改任何表结构
#### Scenario: 最终结构缺失时启动
- **WHEN** 任一必需列、`douyin_account` 表或 `wechat_account` 表缺失
- **THEN** 应用 MUST 停止启动并给出明确错误信息
\ No newline at end of file
## 1. 提交前准备
- [ ] 1.1 重新检查工作区未提交差异,确认本变更不会覆盖既有前端、企微维护或启动脚本改动。
- [ ] 1.2 备份预览库 `wx_phone` 的结构和数据,并保存 `SHOW CREATE TABLE wx_phone` 输出。
- [ ] 1.3 检查现有 `wx_data``phone_number` 索引;缺失时仅新增索引,不修改 `wx_data.account` 数据。
## 2. 预览库结构与后端契约
- [ ] 2.1 编写一次性预览库迁移 SQL:套餐列直接改名,旧 `wechat` 布尔列替换为可空文本,账号结果列调整为可空文本,创建 `douyin_account``wechat_account` 和手机号索引。
- [ ] 2.2 将 `PhoneMybatisConfig` 从自动改表调整为最终结构检查;缺列或缺表时明确阻止启动。
- [ ] 2.3 将 `WxPhoneEntity``WxPhoneSaveRequest``WxPhoneService` 改为最终套餐字段,移除普通保存的三类账号字段和旧微信开关字段。
- [ ] 2.4 让普通新增初始化三个账号结果为空,让普通编辑保留数据库已有账号结果;旧字段或账号字段进入普通保存接口时返回 400。
- [ ] 2.5 为本次修改和新增的每个方法补齐中文新手注释:代码作用、关联文件、调用链或数据流。
## 3. 三类账号查询与手动同步
- [ ] 3.1 新建抖音和微信来源查询 Mapper,按 `phone_number``id ASC` 返回账号列表。
- [ ] 3.2 扩展现有 `WxDataMapper`,按 `phone_number``id ASC` 返回 `account` 列表,作为企业微信来源;不得新建企业微信来源表。
- [ ] 3.3 新建同步请求、结果 DTO 和同步服务:三类查询全部成功后,再一次更新 `wx_phone`;无有效来源保留原值;单卡失败不写入,批量继续。
- [ ] 3.4 在 `WxPhoneController` 新增 `POST /api/wx-phones/account-sync`,仅接受明确 `phoneIds`
- [ ] 3.5 为来源查询、账号合并、单卡同步和批量汇总等每个新增或修改方法补齐中文新手注释。
## 4. 前端展示与同步交互
- [ ] 4.1 将字段适配器、详情抽屉和列表从微信布尔状态改为微信账号文本,并展示抖音、微信、企业微信三个结果字段。
- [ ] 4.2 从手机号新增/编辑弹窗移除三类账号输入和“开通微信”开关;保号套餐改为“已办理 / 未办理”。
- [ ] 4.3 移除列表对三类账号的行内编辑,保留只读显示。
- [ ] 4.4 新增同步按钮和确认弹窗:勾选优先;无勾选时按筛选状态发送筛选结果或全量 ID;目标为空时不调用接口。
- [ ] 4.5 显示同步汇总,成功后刷新列表;失败明细保留给用户查看。
- [ ] 4.6 为目标计算、确认、请求、结果提示和列表刷新等每个新增或修改方法补齐中文新手注释。
## 5. 自动化验证与验收
- [ ] 5.1 新增后端测试:多行来源合并、`wx_data.account` 企业微信复用、空来源保留、单卡失败不写入、批量失败隔离、普通保存拒绝旧字段和账号字段。
- [ ] 5.2 执行 `mvn test``mvn package``npm run build`,记录结果。
- [ ] 5.3 本地 smoke 验证单条同步、勾选同步、筛选结果同步、无筛选全量同步、空来源保留和列表回显。
- [ ] 5.4 记录迁移前备份位置、实际迁移结果、验证证据和未执行项原因。
\ No newline at end of file
schema: spec-driven
# Project context (optional)
# This is shown to AI when creating artifacts.
# Add your tech stack, conventions, style guides, domain knowledge, etc.
# Example:
# context: |
# Tech stack: TypeScript, React, Node.js
# We use conventional commits
# Domain: e-commerce platform
# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
# Example:
# rules:
# proposal:
# - Keep proposals under 500 words
# - Always include a "Non-goals" section
# tasks:
# - Break tasks into chunks of max 2 hours
......@@ -9,6 +9,9 @@
"jsdom": "^29.1.1",
"vue": "^3.5.39",
"vue-router": "^4.6.4"
},
"devDependencies": {
"vite": "^7.3.6"
}
},
"node_modules/@asamuzakjp/css-color": {
......@@ -268,6 +271,448 @@
"vue": "^3.2.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@exodus/bytes": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
......@@ -327,6 +772,363 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
"integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
"integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
"integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
"integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
"integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
"integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
"integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
"integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
"integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
"integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
"integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
"integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
"integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
"integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
"integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
"integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
"integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
"integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
"integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
"integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
"integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
"integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
"integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
"integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
"integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.24",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
......@@ -601,12 +1403,87 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/html-encoding-sniffer": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
......@@ -760,6 +1637,19 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
......@@ -806,6 +1696,51 @@
"node": ">=0.10.0"
}
},
"node_modules/rollup": {
"version": "4.62.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
"integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.9"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.62.2",
"@rollup/rollup-android-arm64": "4.62.2",
"@rollup/rollup-darwin-arm64": "4.62.2",
"@rollup/rollup-darwin-x64": "4.62.2",
"@rollup/rollup-freebsd-arm64": "4.62.2",
"@rollup/rollup-freebsd-x64": "4.62.2",
"@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
"@rollup/rollup-linux-arm-musleabihf": "4.62.2",
"@rollup/rollup-linux-arm64-gnu": "4.62.2",
"@rollup/rollup-linux-arm64-musl": "4.62.2",
"@rollup/rollup-linux-loong64-gnu": "4.62.2",
"@rollup/rollup-linux-loong64-musl": "4.62.2",
"@rollup/rollup-linux-ppc64-gnu": "4.62.2",
"@rollup/rollup-linux-ppc64-musl": "4.62.2",
"@rollup/rollup-linux-riscv64-gnu": "4.62.2",
"@rollup/rollup-linux-riscv64-musl": "4.62.2",
"@rollup/rollup-linux-s390x-gnu": "4.62.2",
"@rollup/rollup-linux-x64-gnu": "4.62.2",
"@rollup/rollup-linux-x64-musl": "4.62.2",
"@rollup/rollup-openbsd-x64": "4.62.2",
"@rollup/rollup-openharmony-arm64": "4.62.2",
"@rollup/rollup-win32-arm64-msvc": "4.62.2",
"@rollup/rollup-win32-ia32-msvc": "4.62.2",
"@rollup/rollup-win32-x64-gnu": "4.62.2",
"@rollup/rollup-win32-x64-msvc": "4.62.2",
"fsevents": "~2.3.2"
}
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
......@@ -833,6 +1768,23 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tldts": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.5.tgz",
......@@ -884,6 +1836,81 @@
"node": ">=20.18.1"
}
},
"node_modules/vite": {
"version": "7.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
"integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.27.0 || ^0.28.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"lightningcss": "^1.21.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/vue": {
"version": "3.5.39",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz",
......
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"engines": {
"node": ">=20.19.0 || >=22.12.0"
},
"dependencies": {
"element-plus": "^2.14.2",
"jsdom": "^29.1.1",
"vue": "^3.5.39",
"vue-router": "^4.6.4"
},
"devDependencies": {
"vite": "^7.3.6"
}
}
import * as Vue from 'vue/dist/vue.esm-bundler.js';
import * as VueRouter from 'vue-router';
import * as ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import '../styles-v2.css';
/**
* 代码作用(白话):列出旧页面必须按顺序加载的模块与其加载后应提供的全局对象;关联文件:assets/index.html、src/router/index.js、app-v2.js;关联逻辑(调用链/消息链/数据流):bootstrap() -> loadLegacyModules() -> window 全局能力检查 -> startCompatibilityPage()。
*/
const legacyModules = [
{ source: './modules/domain/domain-add-dialog.js', expectedGlobal: 'DomainAddDialogAPI' },
{ source: './modules/domain/domain-list-runtime.js', expectedGlobal: 'DomainListRuntime' },
{ source: './modules/overview/overview-list-runtime.js', expectedGlobal: 'OverviewListRuntime' },
{ source: './modules/wechat/wechat-add-dialog.js', expectedGlobal: 'WechatAddDialogAPI' },
{ source: './modules/wechat/wechat-detail-drawer.js', expectedGlobal: 'WechatDetailDrawerAPI' },
{ source: './modules/wechat/wechat-list-runtime.js', expectedGlobal: 'WechatListRuntime' },
{ source: './modules/phone/phone-add-dialog.js', expectedGlobal: 'PhoneAddDialogAPI' },
{ source: './modules/phone/phone-detail-drawer.js', expectedGlobal: 'PhoneDetailDrawerAPI' },
{ source: './modules/phone/phone-image-cell.js', expectedGlobal: 'PhoneCardImageCell' },
{ source: './modules/phone/phone-list-runtime.js', expectedGlobal: 'PhoneListRuntime' },
{ source: './modules/shared/record-adapters.js', expectedGlobal: 'RecordAdapters' },
{ source: './modules/shared/wechat-api-client.js', expectedGlobal: 'WechatApiClient' },
{ source: './modules/shared/phone-api-client.js', expectedGlobal: 'PhoneApiClient' },
{ source: './router/index.js', expectedGlobal: 'initRouterApp' },
{ source: '../app-v2.js', expectedGlobal: 'initPage' }
];
/**
* 代码作用(白话):把 Vite 导入的框架对象放回旧脚本依赖的 window 位置;关联文件:src/router/index.js、src/modules/**、app-v2.js;关联逻辑(调用链/消息链/数据流):bootstrap() -> exposeFrameworkGlobals() -> 旧模块读取 window.Vue/window.VueRouter/window.ElementPlus。
*/
function exposeFrameworkGlobals() {
window.Vue = Vue;
window.VueRouter = VueRouter;
window.ElementPlus = ElementPlus;
}
/**
* 代码作用(白话):根据固定模块标识动态导入一个旧脚本,保留其既有的 window 全局模块模式;关联文件:src/main.js、src/modules/**、src/router/index.js、app-v2.js;关联逻辑(调用链/消息链/数据流):loadLegacyModules() -> importLegacyModule() -> 浏览器加载旧模块 -> 模块挂载 window 对象。
*/
async function importLegacyModule(source) {
switch (source) {
case './modules/domain/domain-add-dialog.js': return import('./modules/domain/domain-add-dialog.js');
case './modules/domain/domain-list-runtime.js': return import('./modules/domain/domain-list-runtime.js');
case './modules/overview/overview-list-runtime.js': return import('./modules/overview/overview-list-runtime.js');
case './modules/wechat/wechat-add-dialog.js': return import('./modules/wechat/wechat-add-dialog.js');
case './modules/wechat/wechat-detail-drawer.js': return import('./modules/wechat/wechat-detail-drawer.js');
case './modules/wechat/wechat-list-runtime.js': return import('./modules/wechat/wechat-list-runtime.js');
case './modules/phone/phone-add-dialog.js': return import('./modules/phone/phone-add-dialog.js');
case './modules/phone/phone-detail-drawer.js': return import('./modules/phone/phone-detail-drawer.js');
case './modules/phone/phone-image-cell.js': return import('./modules/phone/phone-image-cell.js');
case './modules/phone/phone-list-runtime.js': return import('./modules/phone/phone-list-runtime.js');
case './modules/shared/record-adapters.js': return import('./modules/shared/record-adapters.js');
case './modules/shared/wechat-api-client.js': return import('./modules/shared/wechat-api-client.js');
case './modules/shared/phone-api-client.js': return import('./modules/shared/phone-api-client.js');
case './router/index.js': return import('./router/index.js');
case '../app-v2.js': return import('../app-v2.js');
default: throw new Error(`未知旧模块:${source}`);
}
}
/**
* 代码作用(白话):在旧模块无法导入或没有提供约定全局对象时显示可定位的启动错误,避免半成品页面继续运行;关联文件:src/main.js、src/router/index.js、app-v2.js;关联逻辑(调用链/消息链/数据流):loadLegacyModules()/bootstrap() 失败 -> showStartupFailure() -> 页面展示来源文件和缺失对象或错误原因。
*/
function showStartupFailure(source, expectedGlobal, error) {
const message = expectedGlobal
? `无法启动资产后台:${source} 未提供 window.${expectedGlobal}`
: `无法启动资产后台:${source} 加载失败(${error.message})`;
console.error(message, error);
document.body.replaceChildren();
const errorPanel = document.createElement('pre');
errorPanel.textContent = message;
errorPanel.style.cssText = 'margin:24px;padding:16px;color:#991b1b;background:#fef2f2;border:1px solid #fecaca;white-space:pre-wrap;';
document.body.append(errorPanel);
}
/**
* 代码作用(白话):按页面原有依赖顺序加载全部旧模块,并确认每一步都已挂载预期全局对象;关联文件:src/modules/**、src/router/index.js、app-v2.js;关联逻辑(调用链/消息链/数据流):bootstrap() -> loadLegacyModules() -> importLegacyModule() -> window 对象检查 -> startCompatibilityPage()。
*/
async function loadLegacyModules() {
for (const legacyModule of legacyModules) {
try {
await importLegacyModule(legacyModule.source);
} catch (error) {
showStartupFailure(legacyModule.source, '', error);
return false;
}
if (!window[legacyModule.expectedGlobal]) {
showStartupFailure(legacyModule.source, legacyModule.expectedGlobal, new Error('缺少全局对象'));
return false;
}
}
return true;
}
/**
* 代码作用(白话):在全部旧模块就绪后先等待路由解析当前 Hash 地址,再启动页面逻辑,替代旧的 DOMContentLoaded 自动启动;关联文件:src/router/index.js、app-v2.js;关联逻辑(调用链/消息链/数据流):bootstrap() -> startCompatibilityPage() -> await window.initRouterApp() -> window.initPage()。
*/
async function startCompatibilityPage() {
await window.initRouterApp();
window.initPage();
}
/**
* 代码作用(白话):作为 Vite 页面唯一启动入口,确保框架、旧模块和初始化动作按安全顺序执行;关联文件:index.html、src/main.js、src/router/index.js、app-v2.js;关联逻辑(调用链/消息链/数据流):index.html module script -> bootstrap() -> 全局对象 -> 旧模块 -> 路由与页面启动。
*/
async function bootstrap() {
exposeFrameworkGlobals();
if (await loadLegacyModules()) {
await startCompatibilityPage();
}
}
void bootstrap();
\ No newline at end of file
/**
* 代码作用(白话):把手机号接口能力挂载到页面共用对象,并统一使用本地后端 8888 端口;关联文件:F:/Project/xyw_console/src/modules/shared/wechat-api-client.js、F:/Project/xyw_console/start.bat;关联逻辑(调用链/消息链/数据流):页面模块 -> window.PhoneApiClient -> http://localhost:8888/api/wx-phones -> Spring Boot 后端
* 代码作用(白话):把手机号接口能力挂载到页面共用对象,并使用相对 `/api` 经 Vite 代理访问本机后端;关联文件:F:/Project/xyw_console/vite.config.js、F:/Project/xyw_console/src/modules/shared/wechat-api-client.js;关联逻辑(调用链/消息链/数据流):页面模块 -> window.PhoneApiClient -> /api/wx-phones -> Vite :5173 -> Spring Boot :8888
*/
(function attachPhoneApiClient() {
const BASE_URL = 'http://localhost:8888/api/wx-phones';
const BASE_URL = '/api/wx-phones';
/**
* 代码作用(白话):统一处理手机号接口返回,遇到后端报错时直接抛出明确错误,避免列表和弹窗各自重复判断;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/phone/phone-list-runtime.js、F:/Project/xyw_console/src/modules/phone/phone-add-dialog.js;关联逻辑(调用链/消息链/数据流):listPhones()/savePhone()/deletePhone() -> parseJsonResponse() -> app-v2.js/手机号模块错误提示。
......
/**
* 代码作用(白话):把企微接口能力挂载到页面共用对象,并统一使用本地后端 8888 端口;关联文件:F:/Project/xyw_console/src/modules/shared/phone-api-client.js、F:/Project/xyw_console/start.bat;关联逻辑(调用链/消息链/数据流):企微页面 -> window.WechatApiClient -> http://localhost:8888/api/wechat-records -> Spring Boot 后端
/**
* 代码作用(白话):把企微接口能力挂载到页面共用对象,并使用相对 `/api` 经 Vite 代理访问本机后端;关联文件:F:/Project/xyw_console/vite.config.js、F:/Project/xyw_console/src/modules/shared/phone-api-client.js;关联逻辑(调用链/消息链/数据流):企微页面 -> window.WechatApiClient -> /api/wechat-records -> Vite :5173 -> Spring Boot :8888
*/
(function attachWechatApiClient() {
// 当前企微资料接口对应测试数据库;后续切换到生产数据库(正式业务库)时,需要同步改成生产环境对应的后端配置。
const BASE_URL = 'http://localhost:8888/api/wechat-records';
const BASE_URL = '/api/wechat-records';
/**
* 代码作用(白话):统一解析企微接口返回,保证前端只认 code/message/data 这一套结构;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js、F:/Project/xyw_console/src/modules/wechat/wechat-list-runtime.js;关联逻辑(调用链/消息链/数据流):listWechatRecords()/getWechatRecord()/saveWechatRecord() -> parseJsonResponse() -> 页面列表/弹窗提示。
......
......@@ -19,7 +19,7 @@ const LegacyShellTemplate = `
function createLegacyWrapper(moduleName) {
return {
template: LegacyShellTemplate,
/** 代码作用(白话):路由页面挂载后,把旧版 app-v2 的状态和渲染入口重新接到当前 DOM 上,这样切页后还能沿用原来的页面渲染;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/index.html;关联逻辑:调用链是“RouterView mounted -> updateDOMRefs()/refreshDOMRefs() -> state.activeModule 切换 -> renderModule() -> updateActiveNav()”。 */
/** 代码作用(白话):路由页面挂载后,把旧版 app-v2 的状态和渲染入口重新接到当前 DOM 上,这样切页后还能沿用原来的页面渲染;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/assets/index.html;关联逻辑:调用链是“RouterView mounted -> updateDOMRefs()/refreshDOMRefs() -> state.activeModule 切换 -> renderModule() -> updateActiveNav()”。 */
mounted() {
const domRefUpdater = window.updateDOMRefs || window.refreshDOMRefs;
if (typeof domRefUpdater === 'function') {
......@@ -65,7 +65,9 @@ const router = window.VueRouter.createRouter({
const app = window.Vue.createApp({});
app.use(router);
/** 代码作用(白话):拦截左侧导航点击,把旧按钮的 `data-module` 映射成 Vue Router 路由地址,避免旧脚本和新路由重复处理一次点击;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/index.html;关联逻辑:消息链是“sidebar click -> handleSidebarRouteClick() -> router.push() -> createLegacyWrapper().mounted()”。 */
let routerAppInitialized = false;
/** 代码作用(白话):拦截左侧导航点击,把旧按钮的 `data-module` 映射成 Vue Router 路由地址,避免旧脚本和新路由重复处理一次点击;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/assets/index.html;关联逻辑:消息链是“sidebar click -> handleSidebarRouteClick() -> router.push() -> createLegacyWrapper().mounted()”。 */
function handleSidebarRouteClick(event) {
const button = event.target.closest('.nav-link[data-module]');
if (!button) return;
......@@ -81,14 +83,22 @@ function handleSidebarRouteClick(event) {
event.stopPropagation();
}
/** 代码作用(白话):在页面初次加载时绑定路由壳层需要的导航事件,并把整个 Vue Router 应用挂到主容器;关联文件:F:/Project/xyw_console/index.html、F:/Project/xyw_console/app-v2.js;关联逻辑:调用链是“DOMContentLoaded -> initRouterApp() -> 绑定 sidebar 路由点击 -> app.mount('#vue-router-app')”。 */
function initRouterApp() {
/**
* 代码作用(白话):在所有旧模块加载完成后只绑定一次导航事件并挂载一次 Vue Router,并等待当前 Hash 路由就绪,避免重复挂载或页面初始化过早;关联文件:src/main.js、index.html、app-v2.js;关联逻辑(调用链/消息链/数据流):src/main.js -> window.initRouterApp() -> app.mount() -> router.isReady() -> window.initPage()。
*/
async function initRouterApp() {
if (routerAppInitialized) {
return;
}
const sidebar = document.getElementById('sidebar');
if (sidebar) {
sidebar.addEventListener('click', handleSidebarRouteClick, true);
const appRoot = document.getElementById('vue-router-app');
if (!sidebar || !appRoot) {
throw new Error('路由壳层缺失:未找到 sidebar 或 vue-router-app');
}
app.mount('#vue-router-app');
sidebar.addEventListener('click', handleSidebarRouteClick, true);
app.mount(appRoot);
routerAppInitialized = true;
await router.isReady();
}
document.addEventListener('DOMContentLoaded', initRouterApp);
window.initRouterApp = initRouterApp;
\ No newline at end of file
......@@ -30,7 +30,7 @@ echo.
if exist "%FRONTEND_SCRIPT%" (
echo [2/2] Starting frontend in a separate window...
start "XYW Frontend" "%FRONTEND_SCRIPT%"
start "" "%FRONTEND_URL%/#/phone-card"
start "" "%FRONTEND_URL%/assets/#/phone-card"
) else (
echo [Warning] Missing frontend launcher: %FRONTEND_SCRIPT%
)
......
/**
* 文件用途(白话):统一定义本地 Vite 服务与构建预览的访问地址,并把前端 `/api` 请求转发给本机 Spring Boot。
* 关联文件:package.json、src/main.js、src/modules/shared/wechat-api-client.js、src/modules/shared/phone-api-client.js。
* 关联逻辑:浏览器 `/assets/` -> Vite :5173/:4173;浏览器 `/api/...` -> Vite 代理 -> Spring Boot :8888。
*/
import { defineConfig } from 'vite';
export default defineConfig({
base: '/assets/',
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false,
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
},
server: {
port: 5173,
strictPort: true,
proxy: {
'/api': {
target: 'http://localhost:8888',
changeOrigin: true
}
}
},
preview: {
port: 4173,
strictPort: true
}
});
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment