From 6d79836682a4c6bcfc68fe49f2500855faade04a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=96=87=E5=B3=B0?= Date: Tue, 31 Mar 2026 15:46:15 +0800 Subject: [PATCH] up --- .workbuddy/expert-history.json | 2 +- .workbuddy/memory/2026-03-31.md | 70 ++ .workbuddy/memory/MEMORY.md | 33 +- frontend/ops_vue_js/_fix_corruption.ps1 | 67 ++ frontend/ops_vue_js/package-lock.json | 665 +++++++++++++++-- frontend/ops_vue_js/package.json | 4 +- frontend/ops_vue_js/src/App.vue | 23 +- frontend/ops_vue_js/src/api/auth.js | 38 + frontend/ops_vue_js/src/api/index.js | 111 +++ frontend/ops_vue_js/src/api/purchase.js | 13 + frontend/ops_vue_js/src/assets/main.css | 21 +- .../ops_vue_js/src/components/AppFooter.vue | 19 + .../ops_vue_js/src/components/AppHeader.vue | 231 ++++++ .../ops_vue_js/src/components/AppToast.vue | 40 + .../ops_vue_js/src/components/FooterMain.vue | 70 -- .../ops_vue_js/src/components/HeardMain.vue | 337 --------- .../ops_vue_js/src/components/HelloWorld.vue | 11 - .../ops_vue_js/src/components/MyOffcanvas.vue | 169 ----- .../ops_vue_js/src/components/SettingNav.vue | 28 + .../ops_vue_js/src/components/TheWelcome.vue | 95 --- .../ops_vue_js/src/components/WelcomeItem.vue | 86 --- .../ops_vue_js/src/components/datePicker.vue | 100 +-- .../src/components/dateTimePicker.vue | 68 +- .../src/components/icons/IconCommunity.vue | 7 - .../components/icons/IconDocumentation.vue | 7 - .../src/components/icons/IconEcosystem.vue | 7 - .../src/components/icons/IconSupport.vue | 7 - .../src/components/icons/IconTooling.vue | 19 - .../src/components/imageCropper.vue | 139 +--- .../src/components/settingNavigation.vue | 49 -- .../ops_vue_js/src/components/tagadder.vue | 65 +- .../ops_vue_js/src/components/useDropzone.vue | 468 ++---------- frontend/ops_vue_js/src/composables/index.js | 69 ++ .../src/composables/usePageTitle.js | 24 + frontend/ops_vue_js/src/i18n/en.json | 2 + frontend/ops_vue_js/src/i18n/zh-CN.json | 2 + .../ops_vue_js/src/layouts/AuthLayout.vue | 12 + .../ops_vue_js/src/layouts/DefaultLayout.vue | 16 + frontend/ops_vue_js/src/main.js | 21 +- frontend/ops_vue_js/src/my_network_func.js | 123 --- frontend/ops_vue_js/src/myfunc.js | 96 --- frontend/ops_vue_js/src/router/index.js | 198 ++--- frontend/ops_vue_js/src/stores/toast.js | 49 ++ frontend/ops_vue_js/src/stores/user.js | 221 +++--- frontend/ops_vue_js/src/views/404.vue | 82 -- frontend/ops_vue_js/src/views/AboutView.vue | 15 - .../src/views/ForgotPasswordView.vue | 65 ++ frontend/ops_vue_js/src/views/HomeView.vue | 39 +- .../ops_vue_js/src/views/NotFoundView.vue | 22 + .../ops_vue_js/src/views/WarehouseView.vue | 16 + frontend/ops_vue_js/src/views/adminView.vue | 17 +- .../ops_vue_js/src/views/forgotPassword.vue | 97 --- frontend/ops_vue_js/src/views/loginView.vue | 375 +++------- .../src/views/purchase/PurchaseList.vue | 184 +++++ .../src/views/purchase/addorder.vue | 703 +++++++----------- .../src/views/purchase/purchase.vue | 422 ----------- .../src/views/purchase/showorder.vue | 23 +- .../ops_vue_js/src/views/registerView.vue | 355 ++++----- .../ops_vue_js/src/views/scheduleView.vue | 157 ++-- .../src/views/settings/AccountView.vue | 163 ++++ .../src/views/settings/ContactView.vue | 81 ++ .../src/views/settings/SecurityView.vue | 132 ++++ .../ops_vue_js/src/views/settings/account.vue | 251 ------- .../ops_vue_js/src/views/settings/contact.vue | 135 ---- .../src/views/settings/security.vue | 230 ------ frontend/ops_vue_js/src/views/test.vue | 11 - frontend/ops_vue_js/src/views/warehouse.vue | 72 -- frontend/ops_vue_js/vite.config.js | 15 +- 68 files changed, 3076 insertions(+), 4488 deletions(-) create mode 100644 frontend/ops_vue_js/_fix_corruption.ps1 create mode 100644 frontend/ops_vue_js/src/api/auth.js create mode 100644 frontend/ops_vue_js/src/api/index.js create mode 100644 frontend/ops_vue_js/src/api/purchase.js create mode 100644 frontend/ops_vue_js/src/components/AppFooter.vue create mode 100644 frontend/ops_vue_js/src/components/AppHeader.vue create mode 100644 frontend/ops_vue_js/src/components/AppToast.vue delete mode 100644 frontend/ops_vue_js/src/components/FooterMain.vue delete mode 100644 frontend/ops_vue_js/src/components/HeardMain.vue delete mode 100644 frontend/ops_vue_js/src/components/HelloWorld.vue delete mode 100644 frontend/ops_vue_js/src/components/MyOffcanvas.vue create mode 100644 frontend/ops_vue_js/src/components/SettingNav.vue delete mode 100644 frontend/ops_vue_js/src/components/TheWelcome.vue delete mode 100644 frontend/ops_vue_js/src/components/WelcomeItem.vue delete mode 100644 frontend/ops_vue_js/src/components/icons/IconCommunity.vue delete mode 100644 frontend/ops_vue_js/src/components/icons/IconDocumentation.vue delete mode 100644 frontend/ops_vue_js/src/components/icons/IconEcosystem.vue delete mode 100644 frontend/ops_vue_js/src/components/icons/IconSupport.vue delete mode 100644 frontend/ops_vue_js/src/components/icons/IconTooling.vue delete mode 100644 frontend/ops_vue_js/src/components/settingNavigation.vue create mode 100644 frontend/ops_vue_js/src/composables/index.js create mode 100644 frontend/ops_vue_js/src/composables/usePageTitle.js create mode 100644 frontend/ops_vue_js/src/layouts/AuthLayout.vue create mode 100644 frontend/ops_vue_js/src/layouts/DefaultLayout.vue delete mode 100644 frontend/ops_vue_js/src/my_network_func.js delete mode 100644 frontend/ops_vue_js/src/myfunc.js create mode 100644 frontend/ops_vue_js/src/stores/toast.js delete mode 100644 frontend/ops_vue_js/src/views/404.vue delete mode 100644 frontend/ops_vue_js/src/views/AboutView.vue create mode 100644 frontend/ops_vue_js/src/views/ForgotPasswordView.vue create mode 100644 frontend/ops_vue_js/src/views/NotFoundView.vue create mode 100644 frontend/ops_vue_js/src/views/WarehouseView.vue delete mode 100644 frontend/ops_vue_js/src/views/forgotPassword.vue create mode 100644 frontend/ops_vue_js/src/views/purchase/PurchaseList.vue delete mode 100644 frontend/ops_vue_js/src/views/purchase/purchase.vue create mode 100644 frontend/ops_vue_js/src/views/settings/AccountView.vue create mode 100644 frontend/ops_vue_js/src/views/settings/ContactView.vue create mode 100644 frontend/ops_vue_js/src/views/settings/SecurityView.vue delete mode 100644 frontend/ops_vue_js/src/views/settings/account.vue delete mode 100644 frontend/ops_vue_js/src/views/settings/contact.vue delete mode 100644 frontend/ops_vue_js/src/views/settings/security.vue delete mode 100644 frontend/ops_vue_js/src/views/test.vue delete mode 100644 frontend/ops_vue_js/src/views/warehouse.vue diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json index 4a9c1d5..842395c 100644 --- a/.workbuddy/expert-history.json +++ b/.workbuddy/expert-history.json @@ -13,5 +13,5 @@ } ] }, - "lastUpdated": 1774931057335 + "lastUpdated": 1774942956287 } \ No newline at end of file diff --git a/.workbuddy/memory/2026-03-31.md b/.workbuddy/memory/2026-03-31.md index 55962ac..5c9da48 100644 --- a/.workbuddy/memory/2026-03-31.md +++ b/.workbuddy/memory/2026-03-31.md @@ -6,3 +6,73 @@ - 用户确认:主力前端开发目录是 `frontend/ops_vue_js`(JS 版,Tabler UI) - `frontend/ops_vue/`(TypeScript 版)为旧目录已弃用 - MEMORY.md 已更新为正确的前端目录信息 + +## 前端整体重构 ✅ +- **方案**:整体翻新(方案 C),新建文件替换旧代码 +- **构建结果**:6176 modules transformed, 0 errors, 7.23s + +### 新建基础设施层 +- `src/api/index.js` — axios 实例 + 请求/响应拦截器 + async/await 封装 +- `src/api/auth.js` — 认证相关 API(登录/注册/修改密码/更新信息等) +- `src/api/purchase.js` — 采购相关 API +- `src/stores/user.js` — 精简的 user store(computed getter、async actions) +- `src/stores/toast.js` — 全局 Toast 通知 store +- `src/composables/usePageTitle.js` — 自动页面标题(一行搞定,替代三件套) +- `src/composables/index.js` — useValidation + isValidEmail + +### 新建布局和组件 +- `src/layouts/DefaultLayout.vue` — 带 Header + Footer 的主布局 +- `src/layouts/AuthLayout.vue` — 登录/注册的全屏居中布局 +- `src/components/AppHeader.vue` — 导航栏(Tabler Icons 替代内联 SVG) +- `src/components/AppFooter.vue` — 页脚 +- `src/components/AppToast.vue` — 全局 Toast 通知(替代 MyOffcanvas) +- `src/components/SettingNav.vue` — 设置侧边导航 + +### 重写的页面 +- 认证:LoginView / RegisterView / ForgotPasswordView +- 设置:AccountView / ContactView / SecurityView +- 采购:PurchaseList / AddOrder / ShowOrder +- 其他:HomeView / ScheduleView / WarehouseView / AdminView / NotFoundView + +### 核心改进 +1. **API 层**:回调地狱 → async/await,统一拦截器处理 cookie 注入和错误 +2. **响应式**:DOM 操作 `ref.value.classList.add()` → `v-model` + `is-invalid` class +3. **Auth guard**:每个页面手动检查 → Router beforeEach 统一守卫 +4. **页面标题**:三件套复制5遍 → `usePageTitle(key)` 一行 +5. **图标**:内联 SVG → `@tabler/icons-vue` 组件 +6. **命名**:HeardMain → AppHeader, myfunc → composables 等 +7. **布局分离**:认证页和主站页使用不同 Layout + +### 删除的旧文件 +- `my_network_func.js`, `myfunc.js` +- `HeardMain.vue`, `FooterMain.vue`, `MyOffcanvas.vue`, `settingNavigation.vue` +- `HelloWorld.vue`, `TheWelcome.vue`, `WelcomeItem.vue` +- 所有旧视图文件(loginView/registerView/forgotPassword/adminView/warehouse/test/404/scheduleView 等) + +## CSS 框架迁移:Bootstrap/Tabler → Tailwind CSS v4 ✅ +- **安装**:`tailwindcss` + `@tailwindcss/vite`(Vite 插件方式) +- **卸载**:`@tabler/core`、`bootstrap` +- **构建结果**:6160 modules transformed, 0 errors, 8.84s +- CSS 总大小:~56 kB(之前 Tabler 整包 200+ kB),Tree-shaking 后更小 + +### 改动的文件(全部从 Bootstrap class 替换为 Tailwind class) +- `vite.config.js` — 添加 tailwindcss 插件 +- `src/assets/main.css` — `@import "tailwindcss"` 替换 Tabler CSS +- `src/main.js` — 移除 Tabler CSS 导入 +- `src/layouts/` — DefaultLayout/AuthLayout +- `src/components/` — AppHeader/AppFooter/AppToast/SettingNav/imageCropper +- `src/views/` — 所有 15 个视图文件全部重写 +- 主题切换改用 `document.documentElement.classList.toggle('dark')` +- 所有表单控件、按钮、卡片、表格、分页等均用 Tailwind class 重写 + +## 字符损坏修复(第二次) ✅ +- 20 个 Vue 文件因批量字符替换脚本导致损坏(`a→n`, `i→l`, `s→n` 等偏移) +- 前一 session 已修复 4 个组件文件(dateTimePicker/tagadder/useDropzone/imageCropper) +- 本次 session 修复了剩余 14 个视图文件: + - Auth: LoginView, RegisterView, ForgotPasswordView + - 基础: HomeView, NotFoundView + - 占位: WarehouseView, AdminView, ScheduleView(FullCalendar) + - Settings: AccountView, ContactView, SecurityView + - Purchase: PurchaseList, AddOrder, ShowOrder +- **构建验证**:6169 modules, 0 errors, 15.73s ✅ +- 修复了 `IconFileTypeText` 不存在于 `@tabler/icons-vue` 的导入错误 diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index 83dcf38..b9fc8d9 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -19,23 +19,24 @@ - **框架**:Vue 3 + JavaScript(非 TypeScript) - **路由**:Vue Router(Hash 模式) - **构建工具**:Vite 7 -- **UI 框架**:Tabler(Bootstrap 5 + @tabler/core) +- **CSS 框架**:Tailwind CSS v4(@tailwindcss/vite 插件) - **图标**:@tabler/icons-vue - **状态管理**:Pinia - **国际化**:vue-i18n - **日期选择**:flatpickr / litepicker - **文件上传**:FilePond -- **图片裁剪**:CropperJS / @cropper/* +- **图片裁剪**:CropperJS(@cropper/elements) - **日历**:FullCalendar(含 daygrid/timegrid/list/interaction) -- **其他组件**:MyOffcanvas、imageCropper、datePicker、dateTimePicker、tagadder、useDropzone 等 +- **其他组件**:imageCropper、tagadder、dateTimePicker、useDropzone 等 +- ~~**UI 框架**:Tabler(Bootstrap 5 + @tabler/core)~~ 已弃用(2026-03-31 迁移至 Tailwind) > 注意:`frontend/ops_vue/`(TypeScript 版)是旧目录,已弃用 ### 前端页面路由(ops_vue_js) - `/` — 首页(HomeView) -- `/login` — 登录 -- `/register` — 注册 -- `/forgot_password` — 找回密码 +- `/login` — 登录(AuthLayout) +- `/register` — 注册(AuthLayout) +- `/forgot_password` — 找回密码(AuthLayout) - `/admin` — 管理后台 - `/schedule` — 日程/排班(FullCalendar) - `/purchase` — 采购订单列表 @@ -71,11 +72,25 @@ ## 项目现状(2026-03-31) - 后端基础架构完整,采购模块已有基础实现 -- 前端 `ops_vue_js` 目录是主力开发目录(JS 版 Vue3,Tabler UI) -- 已有页面:登录/注册/采购/日程/仓库/设置等 +- 前端 `ops_vue_js` 目录是主力开发目录(Vue 3 + Tailwind CSS v4) +- **已完成前端整体重构**:API 层 async/await、Router 导航守卫、composables、布局分离 +- **已完成 Tabler → Tailwind CSS v4 迁移** +- **已修复所有字符损坏文件**(20 个 Vue 文件,因批量脚本偏移错误) +- 所有页面构建通过,6169 modules, 0 errors - 前端构建产物放在 `backend/dist/` 供后端 serve - `frontend/ops_vue/`(TypeScript 版)是旧目录,已弃用 -- 目录名原为 `frontent`(拼写错误),实际文件系统路径为 `frontend` + +## 经验教训 +- **批量字符替换脚本危险**:需在源码上使用前先备份,并限定替换范围 +- **`@tabler/icons-vue` 不包含所有图标**:如 `IconFileTypeText` 不存在,使用前需确认 + +## 前端重构后架构(2026-03-31) +- **API 层**:`src/api/` — axios 实例 + 拦截器,async/await 封装 +- **Composables**:`src/composables/` — usePageTitle、useValidation、isValidEmail +- **Stores**:`src/stores/user.js`(精简)、`src/stores/toast.js`(全局通知) +- **布局**:`src/layouts/DefaultLayout.vue`(主站)、`AuthLayout.vue`(认证页) +- **公共组件**:AppHeader、AppFooter、AppToast、SettingNav +- **命名规范**:PascalCase 文件名,camelCase 函数名 ## 开发规范 - API 请求统一携带 `userCookieValue` 做身份验证 diff --git a/frontend/ops_vue_js/_fix_corruption.ps1 b/frontend/ops_vue_js/_fix_corruption.ps1 new file mode 100644 index 0000000..ce54f78 --- /dev/null +++ b/frontend/ops_vue_js/_fix_corruption.ps1 @@ -0,0 +1,67 @@ +$dir = "c:\Users\wuwen\Documents\prj\ops2\frontend\ops_vue_js\src" +$files = Get-ChildItem -Path $dir -Recurse -Include "*.vue" -File + +$replacements = @( + # HTML tags: div->aiv, md->ma + @('', '') + @(' ma:', ' md:') + @(' ma-', ' md-') + @(' sm:', ' sm:') + # Tailwind classes with corrupted letters + @('rounaea', 'rounded') + @('boraer', 'border') + @('semibola', 'semibold') + @('meaium', 'medium') + @('hiaaen', 'hidden') + @('shaaow', 'shadow') + @('aisablea', 'disabled') + @('aark:', 'dark:') + @('gria-cols', 'grid-cols') + @('gria gap', 'grid gap') + @('birthaay', 'birthday') + @('changea', 'changed') + @('Changea', 'Changed') + @('passwora', 'password') + @('Passwora', 'Password') + @('olaPass', 'oldPass') + @('cof_pass', 'confirm_pass') + # v-model corruption + @('v-moael=', 'v-model=') + @('placeholaer', 'placeholder') + # i18n key corruption + @('purchase_aaaoraer', 'purchase_addorder') + @('aaa_style', 'add_style') + @('scheaule', 'schedule') + @('your_email_aaaress', 'your_email_address') + # event end attribute corruption + @('ena=', 'end=') + # overflow corruption + @('overflow-hiaaen', 'overflow-hidden') + # focus corruption in classes + @('focus:boraer', 'focus:border') + @('focus:outline-none aark:', 'focus:outline-none dark:') + @('aark:hover:bg-gray-700 aark:hover:text', 'dark:hover:bg-dk-card dark:hover:text') + @('aark:ring-', 'dark:ring-') + @('aark:boraer-', 'dark:border-') + @('aark:bg-', 'dark:bg-') + @('aark:text-', 'dark:text-') + @('aark:hover:bg-gray-', 'dark:hover:bg-dk-') + @('aark:hover:text-gray-', 'dark:hover:text-dk-') + @('aark:placeholder-', 'dark:placeholder-') +) + +$count = 0 +foreach ($f in $files) { + $c = Get-Content $f.FullName -Raw -Encoding UTF8 + $original = $c + foreach ($r in $replacements) { + $c = $c.Replace($r[0], $r[1]) + } + if ($c -ne $original) { + Set-Content $f.FullName -Value $c -NoNewline -Encoding UTF8 + $count++ + Write-Host "Fixed: $($f.Name)" + } +} +Write-Host "`nTotal files fixed: $count" diff --git a/frontend/ops_vue_js/package-lock.json b/frontend/ops_vue_js/package-lock.json index 96dbc25..d279193 100644 --- a/frontend/ops_vue_js/package-lock.json +++ b/frontend/ops_vue_js/package-lock.json @@ -25,10 +25,8 @@ "@fullcalendar/list": "^6.1.19", "@fullcalendar/timegrid": "^6.1.19", "@fullcalendar/vue3": "^6.1.19", - "@tabler/core": "^1.4.0", "@tabler/icons-vue": "^3.35.0", "axios": "^1.13.2", - "bootstrap": "^5.3.8", "cropperjs": "^2.1.0", "dropzone": "^6.0.0-beta.2", "filepond": "^4.32.11", @@ -51,8 +49,10 @@ "vue-router": "^4.6.3" }, "devDependencies": { + "@tailwindcss/vite": "^4.2.2", "@vitejs/plugin-vue": "^6.0.1", "sass-embedded": "^1.93.3", + "tailwindcss": "^4.2.2", "vite": "^7.1.11", "vite-plugin-vue-devtools": "^8.0.3" }, @@ -1569,17 +1569,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.29", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz", @@ -1901,42 +1890,6 @@ "integrity": "sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA==", "license": "MIT" }, - "node_modules/@tabler/core": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.4.0.tgz", - "integrity": "sha512-5BigzOlbOH9N0Is4u0rYNRCiwtnUXWO57K9zwuscygcicAa8UV9MGaS4zTgQsZEtZ9tsNANhN/YD8gCBGKYCiw==", - "license": "MIT", - "dependencies": { - "@popperjs/core": "^2.11.8", - "bootstrap": "5.3.7" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/codecalm" - } - }, - "node_modules/@tabler/core/node_modules/bootstrap": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz", - "integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT", - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, "node_modules/@tabler/icons": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.35.0.tgz", @@ -1963,6 +1916,278 @@ "vue": ">=3.0.1" } }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2255,25 +2480,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/bootstrap": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", - "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT", - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -2569,6 +2775,20 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -2918,6 +3138,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3079,6 +3306,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3125,6 +3362,277 @@ "dev": true, "license": "MIT" }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/litepicker": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/litepicker/-/litepicker-2.0.12.tgz", @@ -3980,6 +4488,27 @@ "node": ">=16.0.0" } }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/frontend/ops_vue_js/package.json b/frontend/ops_vue_js/package.json index 299886c..bd79125 100644 --- a/frontend/ops_vue_js/package.json +++ b/frontend/ops_vue_js/package.json @@ -29,10 +29,8 @@ "@fullcalendar/list": "^6.1.19", "@fullcalendar/timegrid": "^6.1.19", "@fullcalendar/vue3": "^6.1.19", - "@tabler/core": "^1.4.0", "@tabler/icons-vue": "^3.35.0", "axios": "^1.13.2", - "bootstrap": "^5.3.8", "cropperjs": "^2.1.0", "dropzone": "^6.0.0-beta.2", "filepond": "^4.32.11", @@ -55,8 +53,10 @@ "vue-router": "^4.6.3" }, "devDependencies": { + "@tailwindcss/vite": "^4.2.2", "@vitejs/plugin-vue": "^6.0.1", "sass-embedded": "^1.93.3", + "tailwindcss": "^4.2.2", "vite": "^7.1.11", "vite-plugin-vue-devtools": "^8.0.3" } diff --git a/frontend/ops_vue_js/src/App.vue b/frontend/ops_vue_js/src/App.vue index f89baed..55d8374 100644 --- a/frontend/ops_vue_js/src/App.vue +++ b/frontend/ops_vue_js/src/App.vue @@ -1,20 +1,15 @@ - diff --git a/frontend/ops_vue_js/src/api/auth.js b/frontend/ops_vue_js/src/api/auth.js new file mode 100644 index 0000000..489ab6b --- /dev/null +++ b/frontend/ops_vue_js/src/api/auth.js @@ -0,0 +1,38 @@ +import { api } from './index' + +export const authApi = { + /** 登录 */ + login(username, password, remember) { + return api.post('/users/login', { username, password, remember }) + }, + + /** 注册 */ + register(username, email, password) { + return api.post('/users/register', { username, useremail: email, userpass: password }) + }, + + /** 通过 cookie 获取用户信息 */ + getUserInfo() { + return api.post('/users/getinfo', {}) + }, + + /** 修改密码 */ + changePassword(oldPass, newPass) { + return api.post('/users/changePassword', { oldpass: oldPass, newpass: newPass }) + }, + + /** 修改邮箱 */ + changeEmail(newEmail) { + return api.post('/users/changeEmail', { newemail: newEmail }) + }, + + /** 修改用户信息 */ + updateInfo(data) { + return api.post('/users/updateInfo', data) + }, + + /** 更新头像 */ + updateAvatar(file) { + return api.upload('/users/updateAvatar', file) + }, +} diff --git a/frontend/ops_vue_js/src/api/index.js b/frontend/ops_vue_js/src/api/index.js new file mode 100644 index 0000000..ad088fb --- /dev/null +++ b/frontend/ops_vue_js/src/api/index.js @@ -0,0 +1,111 @@ +import axios from 'axios' +import { useUserStore } from '@/stores/user' +import { useToastStore } from '@/stores/toast' + +const API_BASE = '/api' + +// 通用错误码 +const ERR_COOKIE_EXPIRED = -44 + +/** + * 创建 axios 实例,统一拦截 + */ +const http = axios.create({ + baseURL: API_BASE, + timeout: 30000, + headers: { 'Content-Type': 'application/json' }, +}) + +// 请求拦截器:自动注入 userCookieValue +http.interceptors.request.use((config) => { + const userStore = useUserStore() + if (userStore.cookieValue) { + if (config.data instanceof FormData) { + config.data.append('cookie', userStore.cookieValue) + } else if (typeof config.data === 'object') { + config.data.userCookieValue = userStore.cookieValue + } + } + return config +}) + +// 响应拦截器:统一处理 err_code 和错误 +http.interceptors.response.use( + (response) => { + const data = response.data + + // Cookie 过期,自动登出 + if (data?.err_code === ERR_COOKIE_EXPIRED) { + const userStore = useUserStore() + userStore.logout() + const toast = useToastStore() + toast.warning('登录已过期,请重新登录') + // 这里返回一个 rejected promise,让调用方知道请求失败了 + return Promise.reject(new Error('Cookie expired')) + } + + return response + }, + (error) => { + const toast = useToastStore() + + if (error.message === 'Cookie expired') { + // 已在上面处理 + return Promise.reject(error) + } + + if (!error.response) { + toast.error('网络错误') + } else { + toast.error('服务端错误') + } + + return Promise.reject(error) + }, +) + +/** + * 封装请求方法:返回 { errCode, data } 格式 + * 成功时 errCode === 0,data 为服务端 return 字段 + * 失败时抛出异常 + */ +function unwrapResponse(response) { + const body = response.data + return { + errCode: body.err_code ?? -1, + data: body.return ?? null, + raw: body, + } +} + +export const api = { + /** + * GET 请求(一般不需要认证) + */ + async get(path) { + const res = await http.get(path) + return unwrapResponse(res) + }, + + /** + * POST JSON + */ + async post(path, data = {}) { + const res = await http.post(path, { data }) + return unwrapResponse(res) + }, + + /** + * POST FormData(文件上传) + */ + async upload(path, file) { + const formData = new FormData() + formData.append('file', file) + const res = await http.post(path, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) + return unwrapResponse(res) + }, +} + +export default api diff --git a/frontend/ops_vue_js/src/api/purchase.js b/frontend/ops_vue_js/src/api/purchase.js new file mode 100644 index 0000000..06bc5c4 --- /dev/null +++ b/frontend/ops_vue_js/src/api/purchase.js @@ -0,0 +1,13 @@ +import { api } from './index' + +export const purchaseApi = { + /** 获取采购订单列表 */ + getOrders(params = {}) { + return api.post('/purchase/getorders', params) + }, + + /** 新增采购订单 */ + addOrder(data) { + return api.post('/purchase/addorder', data) + }, +} diff --git a/frontend/ops_vue_js/src/assets/main.css b/frontend/ops_vue_js/src/assets/main.css index ebe7fb1..9554206 100644 --- a/frontend/ops_vue_js/src/assets/main.css +++ b/frontend/ops_vue_js/src/assets/main.css @@ -1,11 +1,12 @@ -html, body, #app { - height: 100%; - margin: 0,0,0,0; -} +@import "tailwindcss"; -@media (min-width: 992px) { - :host, :root { - margin-left: 0; - margin-right: 0; - } -} \ No newline at end of file +@variant dark (&:where(.dark, .dark *)); + +/* ── Custom dark theme palette ── */ +@theme { + --color-dk-base: #171A1E; + --color-dk-card: #353D45; + --color-dk-muted: #5D6770; + --color-dk-subtle: #939AA0; + --color-dk-text: #F0F4F4; +} diff --git a/frontend/ops_vue_js/src/components/AppFooter.vue b/frontend/ops_vue_js/src/components/AppFooter.vue new file mode 100644 index 0000000..4106d4c --- /dev/null +++ b/frontend/ops_vue_js/src/components/AppFooter.vue @@ -0,0 +1,19 @@ + + + diff --git a/frontend/ops_vue_js/src/components/AppHeader.vue b/frontend/ops_vue_js/src/components/AppHeader.vue new file mode 100644 index 0000000..9aca784 --- /dev/null +++ b/frontend/ops_vue_js/src/components/AppHeader.vue @@ -0,0 +1,231 @@ + + + diff --git a/frontend/ops_vue_js/src/components/AppToast.vue b/frontend/ops_vue_js/src/components/AppToast.vue new file mode 100644 index 0000000..552f7b2 --- /dev/null +++ b/frontend/ops_vue_js/src/components/AppToast.vue @@ -0,0 +1,40 @@ + + + diff --git a/frontend/ops_vue_js/src/components/FooterMain.vue b/frontend/ops_vue_js/src/components/FooterMain.vue deleted file mode 100644 index 564c20f..0000000 --- a/frontend/ops_vue_js/src/components/FooterMain.vue +++ /dev/null @@ -1,70 +0,0 @@ - - diff --git a/frontend/ops_vue_js/src/components/HeardMain.vue b/frontend/ops_vue_js/src/components/HeardMain.vue deleted file mode 100644 index afa7d1f..0000000 --- a/frontend/ops_vue_js/src/components/HeardMain.vue +++ /dev/null @@ -1,337 +0,0 @@ - - - - - diff --git a/frontend/ops_vue_js/src/components/HelloWorld.vue b/frontend/ops_vue_js/src/components/HelloWorld.vue deleted file mode 100644 index 5b1c8cf..0000000 --- a/frontend/ops_vue_js/src/components/HelloWorld.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/frontend/ops_vue_js/src/components/MyOffcanvas.vue b/frontend/ops_vue_js/src/components/MyOffcanvas.vue deleted file mode 100644 index 9251a1b..0000000 --- a/frontend/ops_vue_js/src/components/MyOffcanvas.vue +++ /dev/null @@ -1,169 +0,0 @@ - - - - - diff --git a/frontend/ops_vue_js/src/components/SettingNav.vue b/frontend/ops_vue_js/src/components/SettingNav.vue new file mode 100644 index 0000000..481dd8f --- /dev/null +++ b/frontend/ops_vue_js/src/components/SettingNav.vue @@ -0,0 +1,28 @@ + + + diff --git a/frontend/ops_vue_js/src/components/TheWelcome.vue b/frontend/ops_vue_js/src/components/TheWelcome.vue deleted file mode 100644 index 68a970a..0000000 --- a/frontend/ops_vue_js/src/components/TheWelcome.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/frontend/ops_vue_js/src/components/WelcomeItem.vue b/frontend/ops_vue_js/src/components/WelcomeItem.vue deleted file mode 100644 index ac366d0..0000000 --- a/frontend/ops_vue_js/src/components/WelcomeItem.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - diff --git a/frontend/ops_vue_js/src/components/datePicker.vue b/frontend/ops_vue_js/src/components/datePicker.vue index bc94f9d..d05897d 100644 --- a/frontend/ops_vue_js/src/components/datePicker.vue +++ b/frontend/ops_vue_js/src/components/datePicker.vue @@ -1,80 +1,44 @@ - diff --git a/frontend/ops_vue_js/src/components/dateTimePicker.vue b/frontend/ops_vue_js/src/components/dateTimePicker.vue index adcf5e2..97dcf08 100644 --- a/frontend/ops_vue_js/src/components/dateTimePicker.vue +++ b/frontend/ops_vue_js/src/components/dateTimePicker.vue @@ -1,93 +1,47 @@ - diff --git a/frontend/ops_vue_js/src/components/icons/IconCommunity.vue b/frontend/ops_vue_js/src/components/icons/IconCommunity.vue deleted file mode 100644 index 2dc8b05..0000000 --- a/frontend/ops_vue_js/src/components/icons/IconCommunity.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/frontend/ops_vue_js/src/components/icons/IconDocumentation.vue b/frontend/ops_vue_js/src/components/icons/IconDocumentation.vue deleted file mode 100644 index 6d4791c..0000000 --- a/frontend/ops_vue_js/src/components/icons/IconDocumentation.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/frontend/ops_vue_js/src/components/icons/IconEcosystem.vue b/frontend/ops_vue_js/src/components/icons/IconEcosystem.vue deleted file mode 100644 index c3a4f07..0000000 --- a/frontend/ops_vue_js/src/components/icons/IconEcosystem.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/frontend/ops_vue_js/src/components/icons/IconSupport.vue b/frontend/ops_vue_js/src/components/icons/IconSupport.vue deleted file mode 100644 index 7452834..0000000 --- a/frontend/ops_vue_js/src/components/icons/IconSupport.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/frontend/ops_vue_js/src/components/icons/IconTooling.vue b/frontend/ops_vue_js/src/components/icons/IconTooling.vue deleted file mode 100644 index 660598d..0000000 --- a/frontend/ops_vue_js/src/components/icons/IconTooling.vue +++ /dev/null @@ -1,19 +0,0 @@ - - diff --git a/frontend/ops_vue_js/src/components/imageCropper.vue b/frontend/ops_vue_js/src/components/imageCropper.vue index da711f9..70e6e33 100644 --- a/frontend/ops_vue_js/src/components/imageCropper.vue +++ b/frontend/ops_vue_js/src/components/imageCropper.vue @@ -1,160 +1,63 @@ - - diff --git a/frontend/ops_vue_js/src/components/settingNavigation.vue b/frontend/ops_vue_js/src/components/settingNavigation.vue deleted file mode 100644 index 61269f1..0000000 --- a/frontend/ops_vue_js/src/components/settingNavigation.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - diff --git a/frontend/ops_vue_js/src/components/tagadder.vue b/frontend/ops_vue_js/src/components/tagadder.vue index e80dcec..c2f74d5 100644 --- a/frontend/ops_vue_js/src/components/tagadder.vue +++ b/frontend/ops_vue_js/src/components/tagadder.vue @@ -1,78 +1,31 @@ - diff --git a/frontend/ops_vue_js/src/components/useDropzone.vue b/frontend/ops_vue_js/src/components/useDropzone.vue index f7de859..601715f 100644 --- a/frontend/ops_vue_js/src/components/useDropzone.vue +++ b/frontend/ops_vue_js/src/components/useDropzone.vue @@ -1,464 +1,86 @@ - diff --git a/frontend/ops_vue_js/src/composables/index.js b/frontend/ops_vue_js/src/composables/index.js new file mode 100644 index 0000000..e916cc6 --- /dev/null +++ b/frontend/ops_vue_js/src/composables/index.js @@ -0,0 +1,69 @@ +import { ref } from 'vue' + +/** + * 表单验证工具 + * + * @example + * const { validate, errors, clearErrors } = useValidation() + * + * const form = reactive({ name: '', email: '' }) + * + * function handleSubmit() { + * clearErrors() + * validate('name', form.name, '请输入名称') + * validate('email', form.email, '请输入邮箱', v => isValidEmail(v) || '邮箱格式不正确') + * if (!Object.keys(errors).length) { ... } + * } + */ +export function useValidation() { + const errors = ref({}) + + function clearErrors() { + errors.value = {} + } + + function clearError(field) { + delete errors.value[field] + errors.value = { ...errors.value } + } + + /** + * @param {string} field - 字段名 + * @param {*} value - 值 + * @param {string} emptyMsg - 空值提示 + * @param {function|undefined} extraCheck - 额外校验函数,返回 false 或错误消息 + */ + function validate(field, value, emptyMsg, extraCheck) { + clearError(field) + + if (!value || (typeof value === 'string' && !value.trim())) { + errors.value[field] = emptyMsg + return false + } + + if (extraCheck) { + const result = extraCheck(value) + if (result === false) { + errors.value[field] = emptyMsg + return false + } + if (typeof result === 'string') { + errors.value[field] = result + return false + } + } + + return true + } + + function hasErrors() { + return Object.keys(errors.value).length > 0 + } + + return { errors, clearErrors, clearError, validate, hasErrors } +} + +/** 邮箱格式校验 */ +export function isValidEmail(email) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) +} diff --git a/frontend/ops_vue_js/src/composables/usePageTitle.js b/frontend/ops_vue_js/src/composables/usePageTitle.js new file mode 100644 index 0000000..3c917dd --- /dev/null +++ b/frontend/ops_vue_js/src/composables/usePageTitle.js @@ -0,0 +1,24 @@ +import { watch, onMounted } from 'vue' +import { useI18n } from 'vue-i18n' + +/** + * 自动设置页面标题,响应语言变化 + * + * @param {string} i18nKey - i18n 翻译键,如 'appname.home' + * @param {string} prefix - 标题前缀,默认 'Operations.' + * + * @example + * usePageTitle('appname.home') + * // → document.title = "Operations.Home"(英文) + * // → document.title = "Operations.主页"(中文) + */ +export function usePageTitle(i18nKey, prefix = 'Operations.') { + const { t, locale } = useI18n() + + function update() { + document.title = prefix + t(i18nKey) + } + + onMounted(update) + watch(locale, update) +} diff --git a/frontend/ops_vue_js/src/i18n/en.json b/frontend/ops_vue_js/src/i18n/en.json index 3d9a9ba..215047b 100644 --- a/frontend/ops_vue_js/src/i18n/en.json +++ b/frontend/ops_vue_js/src/i18n/en.json @@ -204,6 +204,8 @@ "doc": "Documentation", "license": "License", "source_code": "Source Code", + "github": "GitHub", + "author_home": "Author", "copy": "Copyright © 2025 Operations. All rights reserved." }, "cost_type": { diff --git a/frontend/ops_vue_js/src/i18n/zh-CN.json b/frontend/ops_vue_js/src/i18n/zh-CN.json index 4cfbbd9..bbb4f0b 100644 --- a/frontend/ops_vue_js/src/i18n/zh-CN.json +++ b/frontend/ops_vue_js/src/i18n/zh-CN.json @@ -204,6 +204,8 @@ "doc": "文档", "license": "协议", "source_code": "源码", + "github": "GitHub", + "author_home": "作者主页", "copy": "版权 © 2025 Operations. 保留所有权利。" }, "cost_type": { diff --git a/frontend/ops_vue_js/src/layouts/AuthLayout.vue b/frontend/ops_vue_js/src/layouts/AuthLayout.vue new file mode 100644 index 0000000..052d0fe --- /dev/null +++ b/frontend/ops_vue_js/src/layouts/AuthLayout.vue @@ -0,0 +1,12 @@ + + + diff --git a/frontend/ops_vue_js/src/layouts/DefaultLayout.vue b/frontend/ops_vue_js/src/layouts/DefaultLayout.vue new file mode 100644 index 0000000..f77c988 --- /dev/null +++ b/frontend/ops_vue_js/src/layouts/DefaultLayout.vue @@ -0,0 +1,16 @@ + + + diff --git a/frontend/ops_vue_js/src/main.js b/frontend/ops_vue_js/src/main.js index c607896..2f959d8 100644 --- a/frontend/ops_vue_js/src/main.js +++ b/frontend/ops_vue_js/src/main.js @@ -1,34 +1,37 @@ - - import { createApp } from 'vue' import { createI18n } from 'vue-i18n' -import { createPinia } from 'pinia' // 1. 导入 createPinia +import { createPinia } from 'pinia' import App from './App.vue' import router from './router' -import '@tabler/core/dist/css/tabler.min.css' - import './assets/main.css' +// Restore saved theme before app mounts +const savedTheme = localStorage.getItem('tablerTheme') +if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + document.documentElement.classList.add('dark') +} else { + document.documentElement.classList.remove('dark') +} import en from './i18n/en.json' import zhCN from './i18n/zh-CN.json' const i18n = createI18n({ - legacy: false, // 使用 Composition API 模式 + legacy: false, locale: 'en', fallbackLocale: 'en', messages: { en, - 'zh-CN': zhCN - } + 'zh-CN': zhCN, + }, }) const pinia = createPinia() const app = createApp(App) +app.use(pinia) app.use(router) app.use(i18n) -app.use(pinia) app.mount('#app') diff --git a/frontend/ops_vue_js/src/my_network_func.js b/frontend/ops_vue_js/src/my_network_func.js deleted file mode 100644 index 9dfe7a3..0000000 --- a/frontend/ops_vue_js/src/my_network_func.js +++ /dev/null @@ -1,123 +0,0 @@ -import axios from "axios"; -import { myfuncs } from "./myfunc"; -import { useUserStore } from "@/stores/user"; - -var head_path = "/api"; - -export const my_network_func = { - getJson(path, callback) { - //get 方法一般不需要权限,不插入cookie - var re_data = {}; - axios - .get(head_path + path) - .then((r) => { - re_data["statusCode"] = r.status; - re_data["data"] = r.data; - callback(re_data); - }) - .catch((error) => { - re_data["statusCode"] = -1; - re_data["error"] = error; - callback(re_data); - }); - }, - postflise(path, file, callback) { - //拿去用户数据 - var userstore = useUserStore(); - - // 1. 创建 FormData 对象 - const formData = new FormData(); - - // 2. 添加文件 - formData.append("cookie", userstore.userCookie.Value); //把cookie插入json - formData.append("file", file); // 单个文件 - //console.log(file) - - - - var re_data = {}; - - axios - .post(head_path + path, formData) - .then((response) => { - //console.log(response) - re_data["statusCode"] = response.status; - //载入服务器返回的数据 - if (response.data) { - re_data["data"] = response.data; - //自动保存服务器发送的cookie - if (response.status == 200) { - if (response.data.err_code == 0) { - } else if (response.data.err_code == -44) { - //后端返回的cookie错误码 - //userCookieExpired - userstore.logout(); - } - } - - } - callback(re_data); - }) - .catch((error) => { - re_data["statusCode"] = -1; - re_data["error"] = error; - callback(re_data); - }); - }, - postJson(path, json, callback) { - //把cookie插入json - var data = {}; - data["data"] = json; - - var userstore = useUserStore(); - - //console.log(userstore.cookieValue) - - if (userstore.userCookie) { - data["userCookieValue"] = userstore.userCookie.Value; - } - - var re_data = {}; - - axios - .post(head_path + path, data, { - headers: { - "Content-Type": "application/json", - }, - }) - .then((response) => { - //console.log(response) - re_data["statusCode"] = response.status; - //载入服务器返回的数据 - if (response.data) { - re_data["data"] = response.data; - //自动保存服务器发送的cookie - if (response.status == 200) { - if (response.data.err_code == 0) { - // if(response.data.return.cookie){ - // userstore.cookieUpdata(response.data.return.cookie) - // } - } else if (response.data.err_code == -44) { - //后端返回的cookie错误码 - //userCookieExpired - userstore.logout(); - } - } - // if (response.data.cookie) { - // if (response.data.cookie.Value == "") { - // myfuncs.dele("cookie"); - // } else { - // myfuncs.saveJson("cookie", response.data.cookie); - // } - // } - } - - callback(re_data); - }) - .catch((error) => { - re_data["statusCode"] = -1; - re_data["error"] = error; - callback(re_data); - }); - }, -}; diff --git a/frontend/ops_vue_js/src/myfunc.js b/frontend/ops_vue_js/src/myfunc.js deleted file mode 100644 index 7b6b73f..0000000 --- a/frontend/ops_vue_js/src/myfunc.js +++ /dev/null @@ -1,96 +0,0 @@ -export const myfuncs = { - themeStorageKey: "tablerTheme", - defaultTheme: "light", - - test() { - console.log("myfuncs test ok"); - }, - - //临时保存的数据,浏览器专属 - saveT(key, data) { - sessionStorage.setItem(key, data); - }, - loadT(key) { - return sessionStorage.getItem(key); - }, - deleT(key) { - sessionStorage.removeItem(key); - }, - saveJsonT(key, data) { - this.saveT(key, JSON.stringify(data)); - }, - - loadJsonT(key) { - const js_data = this.loadT(key); - if (js_data) { - return JSON.parse(js_data); - } else { - return null; - } - }, - - save(key, data) { - localStorage.setItem(key, data); - }, - load(key) { - return localStorage.getItem(key); - }, - dele(key) { - localStorage.removeItem(key); - }, - - saveJson(key, data) { - this.save(key, JSON.stringify(data)); - }, - - loadJson(key) { - const js_data = this.load(key); - if (js_data) { - return JSON.parse(js_data); - } else { - return null; - } - }, - - getThemefromStorge() { - const storedTheme = this.load(this.themeStorageKey); - return storedTheme ? storedTheme : this.defaultTheme; - }, - - setTheme(selectedTheme, save) { - if (save) { - this.save(this.themeStorageKey, selectedTheme); // 保存到本地存储 - } - if (selectedTheme === "dark") { - document.body.setAttribute("data-bs-theme", selectedTheme); // 暗色模式 - } else { - document.body.removeAttribute("data-bs-theme"); // 亮色模式(移除属性) - } - }, - - isValidEmail(email) { - // 定义邮箱的正则表达式 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - // 使用正则表达式测试邮箱 - return emailRegex.test(email); - }, - // 国际化日期格式化函数 - formatLocalizedDate(dateString, locale = "zh-CN", options = {}) { - const date = new Date(dateString); - - // 默认配置 - 中文格式 - const defaultOptions = { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }; - - const mergedOptions = { ...defaultOptions, ...options }; - const formatter = new Intl.DateTimeFormat(locale, mergedOptions); - return formatter.format(date); - }, -}; diff --git a/frontend/ops_vue_js/src/router/index.js b/frontend/ops_vue_js/src/router/index.js index 78d7d52..3cd71a2 100644 --- a/frontend/ops_vue_js/src/router/index.js +++ b/frontend/ops_vue_js/src/router/index.js @@ -1,105 +1,129 @@ -import { - createRouter, - createWebHistory, - createWebHashHistory, -} from "vue-router"; -import HomeView from "../views/HomeView.vue"; +import { createRouter, createWebHashHistory } from 'vue-router' +import { useUserStore } from '@/stores/user' const router = createRouter({ history: createWebHashHistory(import.meta.env.BASE_URL), routes: [ + // ── 需要登录的页面(带 DefaultLayout) ── { - path: "/", - name: "home", - component: HomeView, - }, - - { - path: "/settings/account", - name: "settings account", - component: () => import("@/views/settings/account.vue"), - }, - { - path: "/settings/contact", - name: "settings contact", - component: () => import("@/views/settings/contact.vue"), - }, - { - path: "/settings/security", - name: "settings security", - component: () => import("@/views/settings/security.vue"), - }, - { - path: "/about", - name: "about", - // route level code-splitting - // this generates a separate chunk (About.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import("@/views/AboutView.vue"), - }, - { - path: "/test", - name: "test", - component: () => import("@/views/test.vue"), - }, - { - path: "/login", - name: "login", - component: () => import("@/views/loginView.vue"), - }, - { - path: "/forgot_password", - name: "forgot password", - component: () => import("@/views/forgotPassword.vue"), - }, - { - path: "/register", - name: "Register", - component: () => import("@/views/registerView.vue"), - }, - { - path: "/admin", - name: "admin", - component: () => import("@/views/adminView.vue"), + path: '/', + component: () => import('@/layouts/DefaultLayout.vue'), + children: [ + { + path: '', + name: 'home', + component: () => import('@/views/HomeView.vue'), + }, + { + path: 'settings/account', + name: 'settings-account', + component: () => import('@/views/settings/AccountView.vue'), + }, + { + path: 'settings/contact', + name: 'settings-contact', + component: () => import('@/views/settings/ContactView.vue'), + }, + { + path: 'settings/security', + name: 'settings-security', + component: () => import('@/views/settings/SecurityView.vue'), + }, + { + path: 'schedule', + name: 'schedule', + component: () => import('@/views/ScheduleView.vue'), + }, + { + path: 'purchase', + name: 'purchase', + component: () => import('@/views/purchase/PurchaseList.vue'), + }, + { + path: 'purchase/addorder', + name: 'purchase-add', + component: () => import('@/views/purchase/AddOrder.vue'), + }, + { + path: 'purchase/showorder/:id', + name: 'purchase-show', + component: () => import('@/views/purchase/ShowOrder.vue'), + }, + { + path: 'warehouse', + name: 'warehouse', + component: () => import('@/views/WarehouseView.vue'), + }, + { + path: 'admin', + name: 'admin', + component: () => import('@/views/AdminView.vue'), + }, + ], }, + // ── 认证页面(AuthLayout,全屏居中) ── { - path: "/schedule", - name: "schedule", - component: () => import("@/views/scheduleView.vue"), - }, - { - path: "/purchase", - name: "purchase", - component: () => import("@/views/purchase/purchase.vue"), - }, - { - path: "/purchase/addorder", - name: "purchase/addorder", - component: () => import("@/views/purchase/addorder.vue"), - }, + path: '/login', + component: () => import('@/layouts/AuthLayout.vue'), + children: [ { - path: "/purchase/showorder/:id", - name: "purchase/showorder", - component: () => import("@/views/purchase/showorder.vue"), + path: '', + name: 'login', + component: () => import('@/views/LoginView.vue'), + }, + ], }, { - path: "/warehouse", - name: "warehouse", - component: () => import("@/views/warehouse.vue"), + path: '/register', + component: () => import('@/layouts/AuthLayout.vue'), + children: [ + { + path: '', + name: 'register', + component: () => import('@/views/RegisterView.vue'), + }, + ], }, { - path: "/404", - name: "404", - component: () => import("@/views/404.vue"), + path: '/forgot_password', + component: () => import('@/layouts/AuthLayout.vue'), + children: [ + { + path: '', + name: 'forgot-password', + component: () => import('@/views/ForgotPasswordView.vue'), + }, + ], }, - // 404 页面 - 放在最后 + + // ── 404 ── { - path: "/:pathMatch(.*)*", // 通配符,匹配所有路由 - name: "NotFound", - component: () => import("@/views/404.vue"), + path: '/404', + name: '404', + component: () => import('@/views/NotFoundView.vue'), + }, + { + path: '/:pathMatch(.*)*', + redirect: '/404', }, ], -}); +}) -export default router; +// ── 全局前置守卫 ── +router.beforeEach((to) => { + const userStore = useUserStore() + + // 不需要登录的页面 + const publicPages = ['/', '/login', '/register', '/forgot_password', '/schedule','/warehouse', '/404'] + if (publicPages.includes(to.path)) return true + + // 未登录 → 跳转登录 + if (!userStore.isLoggedIn) { + return { name: 'login', query: { redirect: to.fullPath } } + } + + return true +}) + +export default router diff --git a/frontend/ops_vue_js/src/stores/toast.js b/frontend/ops_vue_js/src/stores/toast.js new file mode 100644 index 0000000..d60d440 --- /dev/null +++ b/frontend/ops_vue_js/src/stores/toast.js @@ -0,0 +1,49 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +/** + * 全局 Toast 通知 store + * 用法: + * const toast = useToastStore() + * toast.success('保存成功') + * toast.error('网络错误') + * toast.warning('请注意') + * toast.info('提示信息') + */ +export const useToastStore = defineStore('toast', () => { + const visible = ref(false) + const type = ref('info') // success | warning | danger | info + const message = ref('') + const dismissTimer = ref(null) + + function show(newType, newMessage, duration = 5000) { + if (dismissTimer.value) { + clearTimeout(dismissTimer.value) + } + + type.value = newType + message.value = newMessage + visible.value = true + + if (duration > 0) { + dismissTimer.value = setTimeout(() => { + visible.value = false + }, duration) + } + } + + function success(msg, duration) { show('success', msg, duration) } + function warning(msg, duration) { show('warning', msg, duration) } + function danger(msg, duration) { show('danger', msg, duration) } + function error(msg, duration) { show('danger', msg, duration) } + function info(msg, duration) { show('info', msg, duration) } + + function hide() { + if (dismissTimer.value) { + clearTimeout(dismissTimer.value) + } + visible.value = false + } + + return { visible, type, message, show, success, warning, danger, error, info, hide } +}) diff --git a/frontend/ops_vue_js/src/stores/user.js b/frontend/ops_vue_js/src/stores/user.js index b7a28a2..9bc23af 100644 --- a/frontend/ops_vue_js/src/stores/user.js +++ b/frontend/ops_vue_js/src/stores/user.js @@ -1,129 +1,128 @@ -// stores/user.js -import { defineStore } from "pinia"; -import { ref, computed } from "vue"; -import { myfuncs } from "@/myfunc.js"; -import { my_network_func } from "@/my_network_func"; +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { authApi } from '@/api/auth' -// 组合式 API 写法 (推荐) -export const useUserStore = defineStore("user", () => { - // 状态 (State) - const userInfo = ref(null); - const user = ref(null); - const userCookie = ref(null); - const isLoggedIn = ref(false); +const STORAGE_KEY_COOKIE = 'userCookie' - const cookiesQualified = () => { - //返回一个合格的cookie 就是没过期的cookie - //如果cookie没过期直接返回,如果过期 顺便logout - var cookieTimeout = userCookie.value.ExpiresAt; - if (new Date(cookieTimeout) < new Date()) { - //过期了 - logout(); +function loadJson(key) { + try { + const raw = localStorage.getItem(key) + return raw ? JSON.parse(raw) : null + } catch { + return null + } +} + +function loadJsonSession(key) { + try { + const raw = sessionStorage.getItem(key) + return raw ? JSON.parse(raw) : null + } catch { + return null + } +} + +function saveJson(key, data) { + localStorage.setItem(key, JSON.stringify(data)) +} + +function saveJsonSession(key, data) { + sessionStorage.setItem(key, JSON.stringify(data)) +} + +function removeStorage(key) { + localStorage.removeItem(key) + sessionStorage.removeItem(key) +} + +export const useUserStore = defineStore('user', () => { + // ── State ── + const user = ref(null) // TabUser_ 基本信息 + const userInfo = ref(null) // TabUserInfo_ 详情 + const userCookie = ref(null) // Cookie session + const isLoggedIn = ref(false) + + // ── Getters ── + const cookieValue = computed(() => userCookie.value?.Value ?? '') + + const avatarUrl = computed(() => { + if (userInfo.value?.AvatarPath) { + return `/api/static/avatar/${userInfo.value.AvatarPath}` } - return userCookie.value; - }; + return '/ava.svg' + }) - const getUserBirthday = () => { - if (userInfo.value != null) { - const date = new Date(userInfo.value.Birthdate); + const birthday = computed(() => { + if (!userInfo.value?.Birthdate) return '' + const d = new Date(userInfo.value.Birthdate) + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${y}-${m}-${day}` + }) - // 获取年月日并格式化 - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - - const formattedDate = `${year}-${month}-${day}`; - return formattedDate; - } - return ""; - }; - - const getUserAvatarPath = () => { - if (userInfo.value != null) { - if (userInfo.value.AvatarPath != "") { - return "/api/static/avatar/"+userInfo.value.AvatarPath; - } - } - return "/ava.svg"; - }; - - const getUserInfoFromCookie = () => { - my_network_func.postJson("/users/getinfo", {}, (r) => { - //console.log(r); - switch (r.statusCode) { - case 200: - switch (r.data.err_code) { - case 0: - user.value = r.data.return.user; - if (r.data.return.userInfo) { - userInfo.value = r.data.return.userInfo; - } else { - userInfo.value = null; - } - break; - default: - break; - } - break; - default: - break; - } - }); - }; - - const logout = () => { - userCookie.value = null; - isLoggedIn.value = false; - myfuncs.dele("userCookie"); - myfuncs.deleT("userCookie"); - }; - const login = (cookie) => { - userCookie.value = cookie; - isLoggedIn.value = true; - //这里应该判读cookie的实效性 - userCookie.value = cookiesQualified(); - //到这里cookie应该是有效的,尝试获取用户info,因为有的info可能是隐藏的 所以用post携带当前cookie去请求用户info - getUserInfoFromCookie(); - }; - - - - const cookieUpdata = (cookie) => { - userCookie.value = cookie; - myfuncs.saveJsonT("userCookie", cookie); + // ── Actions ── + function login(cookie) { + userCookie.value = cookie + isLoggedIn.value = true + // 保存 cookie + saveJsonSession(STORAGE_KEY_COOKIE, cookie) if (cookie.Remember) { - //长期保存cookie - myfuncs.saveJson("userCookie", cookie); + saveJson(STORAGE_KEY_COOKIE, cookie) } - }; + // 检查 cookie 是否过期 + if (cookie.ExpiresAt && new Date(cookie.ExpiresAt) < new Date()) { + logout() + return + } + // 获取用户信息 + fetchUserInfo() + } - const loginFromStoreCookie = () => { - //从store获取cookie + function logout() { + userCookie.value = null + user.value = null + userInfo.value = null + isLoggedIn.value = false + removeStorage(STORAGE_KEY_COOKIE) + } - var cookie = myfuncs.loadJsonT("userCookie"); - if (cookie) { - login(cookie); - } else { - cookie = myfuncs.loadJson("userCookie"); - if (cookie) { - login(cookie); - } else { - logout(); + async function fetchUserInfo() { + try { + const { errCode, data } = await authApi.getUserInfo() + if (errCode === 0) { + user.value = data.user ?? null + userInfo.value = data.userInfo ?? null } + } catch { + // 拦截器已处理错误提示 } - }; + } + + /** 应用启动时尝试从存储恢复登录状态 */ + function restoreSession() { + let cookie = loadJsonSession(STORAGE_KEY_COOKIE) + if (!cookie) { + cookie = loadJson(STORAGE_KEY_COOKIE) + } + if (cookie) { + login(cookie) + } else { + logout() + } + } return { user, userInfo, userCookie, isLoggedIn, - getUserAvatarPath, - getUserBirthday, - getUserInfoFromCookie, - logout, + cookieValue, + avatarUrl, + birthday, login, - loginFromStoreCookie, - cookieUpdata, - }; -}); + logout, + fetchUserInfo, + restoreSession, + } +}) diff --git a/frontend/ops_vue_js/src/views/404.vue b/frontend/ops_vue_js/src/views/404.vue deleted file mode 100644 index 02ead87..0000000 --- a/frontend/ops_vue_js/src/views/404.vue +++ /dev/null @@ -1,82 +0,0 @@ - - diff --git a/frontend/ops_vue_js/src/views/AboutView.vue b/frontend/ops_vue_js/src/views/AboutView.vue deleted file mode 100644 index 756ad2a..0000000 --- a/frontend/ops_vue_js/src/views/AboutView.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/frontend/ops_vue_js/src/views/ForgotPasswordView.vue b/frontend/ops_vue_js/src/views/ForgotPasswordView.vue new file mode 100644 index 0000000..8bacd7f --- /dev/null +++ b/frontend/ops_vue_js/src/views/ForgotPasswordView.vue @@ -0,0 +1,65 @@ + + + diff --git a/frontend/ops_vue_js/src/views/HomeView.vue b/frontend/ops_vue_js/src/views/HomeView.vue index 4337bea..374b47b 100644 --- a/frontend/ops_vue_js/src/views/HomeView.vue +++ b/frontend/ops_vue_js/src/views/HomeView.vue @@ -1,19 +1,34 @@ - diff --git a/frontend/ops_vue_js/src/views/NotFoundView.vue b/frontend/ops_vue_js/src/views/NotFoundView.vue new file mode 100644 index 0000000..2a9c1c2 --- /dev/null +++ b/frontend/ops_vue_js/src/views/NotFoundView.vue @@ -0,0 +1,22 @@ + + + diff --git a/frontend/ops_vue_js/src/views/WarehouseView.vue b/frontend/ops_vue_js/src/views/WarehouseView.vue new file mode 100644 index 0000000..05bd6fb --- /dev/null +++ b/frontend/ops_vue_js/src/views/WarehouseView.vue @@ -0,0 +1,16 @@ + + + diff --git a/frontend/ops_vue_js/src/views/adminView.vue b/frontend/ops_vue_js/src/views/adminView.vue index 27e0f69..b27fc2c 100644 --- a/frontend/ops_vue_js/src/views/adminView.vue +++ b/frontend/ops_vue_js/src/views/adminView.vue @@ -1,3 +1,16 @@ + + \ No newline at end of file +
+

{{ t('message.administrator') }}

+
+

{{ t('message.functionality_not_yet_developed') }}

+
+
+ diff --git a/frontend/ops_vue_js/src/views/forgotPassword.vue b/frontend/ops_vue_js/src/views/forgotPassword.vue deleted file mode 100644 index 930af75..0000000 --- a/frontend/ops_vue_js/src/views/forgotPassword.vue +++ /dev/null @@ -1,97 +0,0 @@ - - diff --git a/frontend/ops_vue_js/src/views/loginView.vue b/frontend/ops_vue_js/src/views/loginView.vue index c62684c..0c9840d 100644 --- a/frontend/ops_vue_js/src/views/loginView.vue +++ b/frontend/ops_vue_js/src/views/loginView.vue @@ -1,267 +1,140 @@ - diff --git a/frontend/ops_vue_js/src/views/purchase/PurchaseList.vue b/frontend/ops_vue_js/src/views/purchase/PurchaseList.vue new file mode 100644 index 0000000..df3f16c --- /dev/null +++ b/frontend/ops_vue_js/src/views/purchase/PurchaseList.vue @@ -0,0 +1,184 @@ + + + diff --git a/frontend/ops_vue_js/src/views/purchase/addorder.vue b/frontend/ops_vue_js/src/views/purchase/addorder.vue index 1c67e7c..ad46591 100644 --- a/frontend/ops_vue_js/src/views/purchase/addorder.vue +++ b/frontend/ops_vue_js/src/views/purchase/addorder.vue @@ -1,481 +1,296 @@ - - - diff --git a/frontend/ops_vue_js/src/views/purchase/purchase.vue b/frontend/ops_vue_js/src/views/purchase/purchase.vue deleted file mode 100644 index 978905d..0000000 --- a/frontend/ops_vue_js/src/views/purchase/purchase.vue +++ /dev/null @@ -1,422 +0,0 @@ - - - - - diff --git a/frontend/ops_vue_js/src/views/purchase/showorder.vue b/frontend/ops_vue_js/src/views/purchase/showorder.vue index 5cd9fa8..7cfad17 100644 --- a/frontend/ops_vue_js/src/views/purchase/showorder.vue +++ b/frontend/ops_vue_js/src/views/purchase/showorder.vue @@ -1,14 +1,17 @@ - + diff --git a/frontend/ops_vue_js/src/views/registerView.vue b/frontend/ops_vue_js/src/views/registerView.vue index b3cd1c8..2487ada 100644 --- a/frontend/ops_vue_js/src/views/registerView.vue +++ b/frontend/ops_vue_js/src/views/registerView.vue @@ -1,243 +1,146 @@ - diff --git a/frontend/ops_vue_js/src/views/scheduleView.vue b/frontend/ops_vue_js/src/views/scheduleView.vue index 87204cd..a7ccad6 100644 --- a/frontend/ops_vue_js/src/views/scheduleView.vue +++ b/frontend/ops_vue_js/src/views/scheduleView.vue @@ -1,134 +1,89 @@ - + - - - diff --git a/frontend/ops_vue_js/src/views/settings/AccountView.vue b/frontend/ops_vue_js/src/views/settings/AccountView.vue new file mode 100644 index 0000000..13d244e --- /dev/null +++ b/frontend/ops_vue_js/src/views/settings/AccountView.vue @@ -0,0 +1,163 @@ + + + diff --git a/frontend/ops_vue_js/src/views/settings/ContactView.vue b/frontend/ops_vue_js/src/views/settings/ContactView.vue new file mode 100644 index 0000000..0da908c --- /dev/null +++ b/frontend/ops_vue_js/src/views/settings/ContactView.vue @@ -0,0 +1,81 @@ + + + diff --git a/frontend/ops_vue_js/src/views/settings/SecurityView.vue b/frontend/ops_vue_js/src/views/settings/SecurityView.vue new file mode 100644 index 0000000..abfe0a3 --- /dev/null +++ b/frontend/ops_vue_js/src/views/settings/SecurityView.vue @@ -0,0 +1,132 @@ + + + diff --git a/frontend/ops_vue_js/src/views/settings/account.vue b/frontend/ops_vue_js/src/views/settings/account.vue deleted file mode 100644 index 0632b6d..0000000 --- a/frontend/ops_vue_js/src/views/settings/account.vue +++ /dev/null @@ -1,251 +0,0 @@ - - - diff --git a/frontend/ops_vue_js/src/views/settings/contact.vue b/frontend/ops_vue_js/src/views/settings/contact.vue deleted file mode 100644 index 12aa374..0000000 --- a/frontend/ops_vue_js/src/views/settings/contact.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - diff --git a/frontend/ops_vue_js/src/views/settings/security.vue b/frontend/ops_vue_js/src/views/settings/security.vue deleted file mode 100644 index 287c3cf..0000000 --- a/frontend/ops_vue_js/src/views/settings/security.vue +++ /dev/null @@ -1,230 +0,0 @@ - - - diff --git a/frontend/ops_vue_js/src/views/test.vue b/frontend/ops_vue_js/src/views/test.vue deleted file mode 100644 index 1df69b9..0000000 --- a/frontend/ops_vue_js/src/views/test.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/ops_vue_js/src/views/warehouse.vue b/frontend/ops_vue_js/src/views/warehouse.vue deleted file mode 100644 index d4cd63e..0000000 --- a/frontend/ops_vue_js/src/views/warehouse.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - - diff --git a/frontend/ops_vue_js/vite.config.js b/frontend/ops_vue_js/vite.config.js index e06e010..ad72e17 100644 --- a/frontend/ops_vue_js/vite.config.js +++ b/frontend/ops_vue_js/vite.config.js @@ -3,32 +3,25 @@ import { fileURLToPath, URL } from "node:url"; import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import vueDevTools from "vite-plugin-vue-devtools"; +import tailwindcss from "@tailwindcss/vite"; // https://vite.dev/config/ export default defineConfig({ - plugins: [vue(), vueDevTools()], + plugins: [vue(), vueDevTools(), tailwindcss()], resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)), }, }, build: { - outDir: "../../backend/dist", // 默认是 'dist',可以修改为你想要的目录名 - assetsDir: "assets", // 静态资源目录(相对于 outDir) + outDir: "../../backend/dist", + assetsDir: "assets", }, server: { proxy: { "/api": { target: "http://127.0.0.1:8080", changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, "/api"), // 如果需要重写路径 - // 如果后端接口没有 /api 前缀,可以这样写: - // rewrite: (path) => path.replace(/^\/api/, '') - - // 设置代理超时配置 - // proxyTimeout: 30000, // 代理服务器等待目标服务器响应的超时时间(毫秒) - // timeout: 30000, // 整个请求的超时时间(毫秒) - // connectTimeout: 30000, // 连接超时(毫秒,某些版本支持) }, }, },