v1.2.55:弹窗统一化——全站手写弹窗迁移到统一 AppDialog 组件 + 国产浏览器/微信内核兼容
ci/woodpecker/tag/build Pipeline was successful
ci/woodpecker/tag/build Pipeline was successful
① 弹窗统一化(核心):新增 AppDialog 组件(reka-ui Dialog 底座),把 13 个视图文件里 34 处手写弹窗全部迁移,全项目手写 role="dialog" 残留归零。 - 目的:以 Dialog 原语换取焦点捕获、Esc 关闭、body 滚动锁定、aria 语义与 Portal 渲染,同时保持与手写弹窗一致的视觉(面板基类 w-full/bg-white/flex-col/overflow-hidden/shadow-xl/max-h-[85vh])。 - 公开 API 与底座解耦:open / variant(sheet|center) / z-class / overlay-class / wrapper-class / panel-class / dismissable / fade / @update:open / 默认插槽。 - 逐处核对:阴影(原无阴影者显式传 shadow-none 抵消基类 shadow-xl、shadow-2xl 显式覆盖)、圆角(非基类值显式覆盖)、fade(原 <Transition name="fade"> 者传 :fade=true)、守卫(dismissable:LeaveView executing/applying、AdminCdks result 视图、AdminPendingSchools rejectSubmitting、HomeView「重新登录」原遮罩不可点故 false)、z-index(z-[60] 等)、定位层边距(wrapper-class px-4/6)。 - fade 实现说明:reka 的 Presence 只识别 CSS animation(读 computed animation-name 并监听节点自身 animationend),不认 Vue 的 class 过渡;故用 CSS animation + reka 输出的 data-state="open|closed" 实现,挂在 DismissableLayer 根节点(即 role="dialog" 节点)。实现在 fade 时额外给 content 根加 relative + 同层 z-class:opacity<1 会形成 z-index:0 的层叠上下文,否则内部 fixed 面板会被 overlay 盖住。 - 迁移固有行为差异:MobileLayout 公告面板失去 slide-up 上滑动画改为 fade;各弹窗新增 Esc 关闭能力(reka 默认);面板固定 overflow-hidden+max-h-[85vh](原无此二类的面板有微调)。非弹窗遮罩(AdminLayout 侧边栏抽屉、右侧滑出公告面板;GuideOverlay 引导层)按判断保留未迁移。 ② 国产浏览器/微信内核兼容(chrome78+): - vite build.target 降到 chrome78(微信 XWEB 常见下限),旧的默认 target 会产出 Chrome 80 以下无法解析的新语法导致白屏。 - 新增 scripts/postbuild-css.mjs 并接入 build:解包 :where、降级 oklch 颜色、剥离 @layer,产出对老内核可用的 CSS;含兜底校验(残留 @layer/oklch/:where 直接失败)。 - index.html 内联补齐 Object.hasOwn(Chrome 93+,reka-ui 与 @vueuse/shared 内部调用;esbuild 只降语法不降运行时 API)。 ③ 收尾修复既有问题:AdminUsers 删除从未使用的 Card 死 import;AdminCdks 修正 copiedResultIdx/resultCopiedIdx 变量名不一致(导致结果视图复制高亮失效并抛 ReferenceError);MobileLayout 原 .slide-up-leave-to 误用 transition(随迁移一并移除,问题消失)。 ④ 验证:vue-tsc --noEmit、vite build(含 postbuild-css)全过;headless Chrome 实测整站启动渲染正常;全项目残留扫描确认无遗留手写弹窗。 ⑤ 本次为纯前端改动(含 reka-ui 依赖),不含后端变更。
This commit is contained in:
@@ -6,6 +6,28 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#f5f5f5" />
|
||||
<title>小助手</title>
|
||||
<script>
|
||||
// 老内核(Chrome 78~92,即微信 XWEB / 国产浏览器)缺失的 ES2022 运行时 API 补齐。
|
||||
// 背景:reka-ui(其弹窗组件)与 @vueuse/shared 内部直接调用 Object.hasOwn
|
||||
// (Chrome 93+),而 esbuild/vite 的 target 只降语法、不降运行时 API,
|
||||
// 构建产物在 Chrome 78 上会在弹窗挂载时抛 TypeError。
|
||||
// 该内联脚本先于 module 产物执行(module 默认 defer),故此处按需补齐即可;
|
||||
// 现代内核因特性检测成立而跳过,零开销。写法保持 ES5,避免老内核解析失败。
|
||||
;(function () {
|
||||
if (typeof Object.hasOwn !== 'function') {
|
||||
Object.defineProperty(Object, 'hasOwn', {
|
||||
value: function (obj, prop) {
|
||||
if (obj === null || obj === undefined) {
|
||||
throw new TypeError('Cannot convert undefined or null to object')
|
||||
}
|
||||
return Object.prototype.hasOwnProperty.call(Object(obj), prop)
|
||||
},
|
||||
configurable: true,
|
||||
writable: true,
|
||||
})
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<script>
|
||||
// 暗色主题防闪烁:与 composables/useTheme.ts 保持同一时间规则
|
||||
// (18:00–次日 06:00 暗色),在首屏渲染前预设 html.dark 与状态栏色
|
||||
|
||||
Generated
+489
-50
@@ -13,6 +13,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^1.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"reka-ui": "^2.10.4",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"vue": "^3.5.30",
|
||||
"vue-router": "^5.0.4"
|
||||
@@ -22,6 +23,7 @@
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"lightningcss": "^1.33.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.1",
|
||||
@@ -159,6 +161,79 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
|
||||
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
|
||||
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.8.0",
|
||||
"@floating-ui/utils": "^0.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
|
||||
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="
|
||||
},
|
||||
"node_modules/@floating-ui/vue": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.11.tgz",
|
||||
"integrity": "sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.6",
|
||||
"@floating-ui/utils": "^0.2.11",
|
||||
"vue-demi": ">=0.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/vue/node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@internationalized/date": {
|
||||
"version": "3.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.4.tgz",
|
||||
"integrity": "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@internationalized/number": {
|
||||
"version": "3.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.8.tgz",
|
||||
"integrity": "sha512-8UmMFia46DUt+k97zKd9fKWXcWHR+k8ae3eYzILETuT2KbIvLyOfac7zesw+sJdRAAZ7Q9pM1Mk22aXp2LD0Ig==",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -474,6 +549,14 @@
|
||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.23",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
|
||||
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz",
|
||||
@@ -489,6 +572,255 @@
|
||||
"tailwindcss": "4.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/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,
|
||||
"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/@tailwindcss/node/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,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/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,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/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,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/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,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/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,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/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,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/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,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/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,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/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,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/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,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node/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,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz",
|
||||
@@ -731,6 +1063,30 @@
|
||||
"vite": "^5.2.0 || ^6 || ^7 || ^8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.17.11",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.11.tgz",
|
||||
"integrity": "sha512-+ILjvtHup6Y2hzQ6YzwMgX1Q+oQpxEGOXCEsCNaPoIP0VxMbizIBTmYTDtkerkIQS8/CbP1BRuyt8V/8BCsy1g==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/vue-virtual": {
|
||||
"version": "3.13.39",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.39.tgz",
|
||||
"integrity": "sha512-AG8sI+6nfrDvN6ixuHJL9LaJTf3//0V5VITZYW3UOaIuSVexnEzsbMu4LH/JhMfRTxwhUXPnLqkl1DlzkZbRtg==",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.17.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^2.7.0 || ^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
|
||||
@@ -764,6 +1120,11 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
|
||||
@@ -962,6 +1323,41 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz",
|
||||
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA=="
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "14.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz",
|
||||
"integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "14.4.0",
|
||||
"@vueuse/shared": "14.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "14.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz",
|
||||
"integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "14.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz",
|
||||
"integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||
@@ -1012,6 +1408,17 @@
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-hidden": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-kit": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
|
||||
@@ -1191,6 +1598,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -1543,9 +1955,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
||||
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
@@ -1558,23 +1970,23 @@
|
||||
"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"
|
||||
"lightningcss-android-arm64": "1.33.0",
|
||||
"lightningcss-darwin-arm64": "1.33.0",
|
||||
"lightningcss-darwin-x64": "1.33.0",
|
||||
"lightningcss-freebsd-x64": "1.33.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.33.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.33.0",
|
||||
"lightningcss-linux-arm64-musl": "1.33.0",
|
||||
"lightningcss-linux-x64-gnu": "1.33.0",
|
||||
"lightningcss-linux-x64-musl": "1.33.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.33.0",
|
||||
"lightningcss-win32-x64-msvc": "1.33.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==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
|
||||
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1592,9 +2004,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
|
||||
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1612,9 +2024,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
|
||||
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1632,9 +2044,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
|
||||
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1652,9 +2064,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
|
||||
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1672,9 +2084,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
|
||||
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1692,9 +2104,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
|
||||
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1712,9 +2124,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
|
||||
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1732,9 +2144,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
|
||||
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1752,9 +2164,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
|
||||
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1772,9 +2184,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
|
||||
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1929,6 +2341,11 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ohash": {
|
||||
"version": "2.0.12",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz",
|
||||
"integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw=="
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
@@ -2098,6 +2515,30 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/reka-ui": {
|
||||
"version": "2.10.4",
|
||||
"resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.10.4.tgz",
|
||||
"integrity": "sha512-kbS5GAbkHkYj0EVKAg5ZPEPndhBjeUFSa1Aq5m5ftQwyHnB1v5tw8X6fcwfKH/ry3StPXblMWM1DW82MVS71cw==",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@floating-ui/vue": "^1.1.6",
|
||||
"@internationalized/date": "^3.5.0",
|
||||
"@internationalized/number": "^3.5.0",
|
||||
"@tanstack/vue-virtual": "^3.12.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"@vueuse/shared": "^14.1.0",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"defu": "^6.1.5",
|
||||
"ohash": "^2.0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/zernonia"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": ">= 3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -2232,9 +2673,7 @@
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build": "vite build && node scripts/postbuild-css.mjs",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -14,6 +14,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^1.0.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"reka-ui": "^2.10.4",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"vue": "^3.5.30",
|
||||
"vue-router": "^5.0.4"
|
||||
@@ -23,6 +24,7 @@
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"lightningcss": "^1.33.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.1",
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// 老内核兼容后处理(构建期执行):
|
||||
//
|
||||
// 背景:Tailwind v4 的产物大量使用较新的 CSS 能力,老内核(国产浏览器/微信内置
|
||||
// 内核,实测线上存在 Chrome 78/85/91/97)会丢弃不认识的部分,表现为“只剩基础
|
||||
// 元素、没有样式”。逐项处理如下:
|
||||
//
|
||||
// ① CSS 级联层 @layer(Chrome 99+):老内核不认识该 at-rule,会把整块工具类
|
||||
// 直接丢弃 —— 这是“无样式”的主因,必须剥离;
|
||||
// ② oklch() 颜色(Chrome 111+):降级为十六进制色值(lightningcss);
|
||||
// ③ :where() 选择器(Chrome 88+):其特异性为 0 的设计依赖较新引擎,
|
||||
// 老内核整条规则丢弃;解包为等价选择器(保持原有特异性位置);
|
||||
// ④ JS 语法(可选链等)由 vite build.target 统一降级,不在此处处理。
|
||||
//
|
||||
// 处理顺序:先解包 :where(lightningcss 不处理它),再整体按目标内核降级,
|
||||
// 最后剥离 @layer 并做完整性校验。
|
||||
import { readdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { transform } from 'lightningcss'
|
||||
|
||||
const assetsDir = resolve(process.cwd(), 'dist/assets')
|
||||
|
||||
// lightningcss 目标版本:Chrome 78(微信 XWEB 常见下限,线上真实存在)
|
||||
const TARGETS = {
|
||||
chrome: 78 << 16,
|
||||
safari: 12 << 16,
|
||||
ios_saf: 12 << 16,
|
||||
android: 78 << 16,
|
||||
}
|
||||
|
||||
/** 解包 :where(...) → 内部选择器(老内核不支持 :where,会整条丢弃规则) */
|
||||
function unwrapWhere(css) {
|
||||
let out = ''
|
||||
let i = 0
|
||||
while (i < css.length) {
|
||||
const m = css.indexOf(':where(', i)
|
||||
if (m < 0) {
|
||||
out += css.slice(i)
|
||||
break
|
||||
}
|
||||
// 避免误伤字符串/注释中的 :where(本项目 CSS 产物中无此类内容,保守处理仍做括号配对)
|
||||
out += css.slice(i, m)
|
||||
let depth = 0
|
||||
let end = -1
|
||||
for (let k = m + 6; k < css.length; k++) {
|
||||
if (css[k] === '(') depth++
|
||||
else if (css[k] === ')') {
|
||||
depth--
|
||||
if (depth === 0) {
|
||||
end = k
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (end < 0) {
|
||||
out += css.slice(m)
|
||||
break
|
||||
}
|
||||
out += css.slice(m + 7, end) // 去掉 ":where(" 与末端 ")"
|
||||
i = end + 1
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** 剥离 @layer 包裹:保留块内规则与顺序(层叠仍由源顺序 + 选择器优先级决定) */
|
||||
function unwrapCascadeLayers(css) {
|
||||
let out = css
|
||||
const re = /@layer\s+[A-Za-z0-9_,\s-]+?\{/g
|
||||
|
||||
// 反复处理,兼容理论上的嵌套 @layer
|
||||
for (let pass = 0; pass < 8; pass++) {
|
||||
const heads = []
|
||||
let m
|
||||
re.lastIndex = 0
|
||||
while ((m = re.exec(out)) !== null) {
|
||||
heads.push({ head: m.index, bodyStart: re.lastIndex - 1 })
|
||||
}
|
||||
if (heads.length === 0) break
|
||||
|
||||
for (let i = heads.length - 1; i >= 0; i--) {
|
||||
const { head, bodyStart } = heads[i]
|
||||
let depth = 0
|
||||
let end = -1
|
||||
for (let k = bodyStart; k < out.length; k++) {
|
||||
if (out[k] === '{') depth++
|
||||
else if (out[k] === '}') {
|
||||
depth--
|
||||
if (depth === 0) {
|
||||
end = k
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (end < 0) continue
|
||||
out = out.slice(0, head) + out.slice(bodyStart + 1, end) + out.slice(end + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除裸 @layer 声明语句(如 `@layer components;`)
|
||||
out = out.replace(/@layer\s+[A-Za-z0-9_,\s-]+;/g, '')
|
||||
return out
|
||||
}
|
||||
|
||||
/** 校验:括号闭合(剥离过程中出现异常时立刻失败,避免把坏 CSS 发上线) */
|
||||
function assertBalanced(css, file) {
|
||||
let depth = 0
|
||||
for (const ch of css) {
|
||||
if (ch === '{') depth++
|
||||
else if (ch === '}') {
|
||||
depth--
|
||||
if (depth < 0) throw new Error(`[postbuild-css] ${file} 括号不闭合(出现多余的 })`)
|
||||
}
|
||||
}
|
||||
if (depth !== 0) throw new Error(`[postbuild-css] ${file} 括号不闭合(缺少 ${depth} 个 })`)
|
||||
}
|
||||
|
||||
const files = (await readdir(assetsDir)).filter((f) => f.endsWith('.css'))
|
||||
if (files.length === 0) {
|
||||
throw new Error('[postbuild-css] dist/assets 下未找到 CSS 产物,构建流程异常')
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const path = join(assetsDir, file)
|
||||
const src = await readFile(path)
|
||||
|
||||
// ① 解包 :where()(lightningcss 不会处理它,需先做)
|
||||
let prepared = Buffer.from(unwrapWhere(src.toString('utf8')))
|
||||
const whereCount = (src.toString('utf8').match(/:where\(/g) || []).length
|
||||
|
||||
// ② 按目标内核降级(oklch 等新颜色函数 → 老内核可识别的色值)
|
||||
const { code } = transform({
|
||||
filename: file,
|
||||
code: prepared,
|
||||
minify: true,
|
||||
targets: TARGETS,
|
||||
})
|
||||
let out = code.toString()
|
||||
|
||||
// ③ 剥离 @layer 包裹
|
||||
out = unwrapCascadeLayers(out)
|
||||
|
||||
// ④ 兜底校验:任何一项残留都说明处理逻辑失效,直接失败避免上线残缺样式
|
||||
if (/@layer/.test(out)) throw new Error(`[postbuild-css] ${file} 仍残留 @layer`)
|
||||
if (/oklch\(/.test(out)) throw new Error(`[postbuild-css] ${file} 仍残留 oklch(`)
|
||||
if (/:where\(/.test(out)) throw new Error(`[postbuild-css] ${file} 仍残留 :where(`)
|
||||
assertBalanced(out, file)
|
||||
|
||||
await writeFile(path, out)
|
||||
console.log(
|
||||
`[postbuild-css] ${file}: ${src.length} → ${out.length} bytes` +
|
||||
`(解包 ${whereCount} 处 :where + 降级颜色 + 剥离 @layer)`
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import { watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import UpdateChecker from '@/components/UpdateChecker.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const { toasts, removeToast } = useMessage()
|
||||
@@ -26,6 +27,9 @@ watch(
|
||||
<template>
|
||||
<router-view />
|
||||
|
||||
<!-- 全局新版本检测(无 UI,发现更新时弹窗提示刷新) -->
|
||||
<UpdateChecker />
|
||||
|
||||
<!-- Toast 容器 -->
|
||||
<Teleport to="body">
|
||||
<div class="fixed top-4 right-4 z-[100] flex flex-col gap-2 max-w-sm">
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
// 版本更新检测:定时拉取首页 index.html,比对 JS/CSS 资源文件名。
|
||||
// 后端每次发布都会重新打包,文件名带内容 hash;资源名变化即说明有新版本,
|
||||
// 弹窗提示用户刷新(确认后立即 reload,拿到新 index.html 与新资源)。
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
|
||||
const CHECK_INTERVAL = 5 * 60 * 1000 // 5 分钟
|
||||
const FIRST_DELAY = 30 * 1000 // 启动 30 秒后首次检查(避免与首屏请求抢占)
|
||||
const DISMISS_KEY = 'xz_update_dismissed_version'
|
||||
|
||||
const showUpdate = ref(false)
|
||||
// 当前页面加载时使用的资源名集合(相对路径)
|
||||
const currentAssets = new Set<string>()
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
let stopped = false
|
||||
let prompting = false
|
||||
let pendingVersion = ''
|
||||
|
||||
/** 从 HTML 文本中提取 /assets/ 下的 js/css 资源名 */
|
||||
function extractAssets(html: string): Set<string> {
|
||||
const out = new Set<string>()
|
||||
const re = /\/assets\/[A-Za-z0-9_.-]+\.(?:js|css)/g
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = re.exec(html)) !== null) {
|
||||
out.add(m[0])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** 读取当前页面已加载的资源名(由 script/link 标签推导,等同当前构建版本) */
|
||||
function collectCurrentAssets(): Set<string> {
|
||||
const out = new Set<string>()
|
||||
document.querySelectorAll<HTMLScriptElement>('script[src]').forEach((el) => {
|
||||
const m = el.src.match(/\/assets\/[A-Za-z0-9_.-]+\.(?:js|css)/)
|
||||
if (m) out.add(m[0])
|
||||
})
|
||||
document.querySelectorAll<HTMLLinkElement>('link[href]').forEach((el) => {
|
||||
const m = el.href.match(/\/assets\/[A-Za-z0-9_.-]+\.(?:js|css)/)
|
||||
if (m) out.add(m[0])
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
async function checkUpdate() {
|
||||
if (stopped || prompting) return
|
||||
try {
|
||||
// 版本探测必须绕过 HTTP 缓存(后端 index.html 有 1 小时 MaxAge),故带时间戳参数
|
||||
const res = await fetch(`/?_v=${Date.now()}`, {
|
||||
cache: 'no-store',
|
||||
headers: { 'Cache-Control': 'no-cache', Pragma: 'no-cache' },
|
||||
})
|
||||
if (!res.ok) return
|
||||
const html = await res.text()
|
||||
const latest = extractAssets(html)
|
||||
if (latest.size === 0 || currentAssets.size === 0) return
|
||||
|
||||
// 任一新资源名未在当前页面出现 → 有新版本
|
||||
const hasNew = [...latest].some((a) => !currentAssets.has(a))
|
||||
if (!hasNew) return
|
||||
|
||||
const newVersion = [...latest].join(',')
|
||||
if (sessionStorage.getItem(DISMISS_KEY) === newVersion) return // 本会话已提示过
|
||||
|
||||
pendingVersion = newVersion
|
||||
prompting = true
|
||||
showUpdate.value = true
|
||||
} catch {
|
||||
// 网络异常静默跳过,等待下一轮
|
||||
} finally {
|
||||
prompting = false
|
||||
}
|
||||
}
|
||||
|
||||
function schedule(delay: number) {
|
||||
if (stopped) return
|
||||
timer = setTimeout(async () => {
|
||||
await checkUpdate()
|
||||
schedule(CHECK_INTERVAL)
|
||||
}, delay)
|
||||
}
|
||||
|
||||
/** 用户确认刷新:立即重载(拿新版本资源) */
|
||||
function reloadNow() {
|
||||
showUpdate.value = false
|
||||
location.reload()
|
||||
}
|
||||
|
||||
/** 用户选择稍后:记录本次版本,本会话内不再重复提示 */
|
||||
function dismiss() {
|
||||
showUpdate.value = false
|
||||
if (pendingVersion) sessionStorage.setItem(DISMISS_KEY, pendingVersion)
|
||||
}
|
||||
|
||||
/** 弹窗关闭统一入口(按钮 / 点遮罩 / Esc 都走这里),关闭即视为「稍后」 */
|
||||
function onDialogOpenChange(value: boolean) {
|
||||
if (value) {
|
||||
showUpdate.value = true
|
||||
return
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentAssets.clear()
|
||||
collectCurrentAssets().forEach((a) => currentAssets.add(a))
|
||||
schedule(FIRST_DELAY)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopped = true
|
||||
if (timer) clearTimeout(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 新版本提示弹窗:标题 + 说明 + 稍后/立即刷新 -->
|
||||
<AppDialog
|
||||
:open="showUpdate"
|
||||
variant="center"
|
||||
z-class="z-[200]"
|
||||
panel-class="w-[86%] max-w-[340px] p-5"
|
||||
title="发现新版本"
|
||||
description="网站已更新,刷新后即可使用最新功能,是否立即刷新?"
|
||||
@update:open="onDialogOpenChange"
|
||||
>
|
||||
<h3 class="text-base font-semibold text-gray-900 mb-2">发现新版本</h3>
|
||||
<p class="text-sm text-gray-500 leading-relaxed mb-5">
|
||||
网站已更新,刷新后即可使用最新功能,是否立即刷新?
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="flex-1 py-2.5 rounded-lg border border-gray-200 text-sm text-gray-600 active:bg-gray-50"
|
||||
@click="dismiss"
|
||||
>
|
||||
稍后
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 py-2.5 rounded-lg bg-blue-600 text-white text-sm font-medium active:bg-blue-700"
|
||||
@click="reloadNow"
|
||||
>
|
||||
立即刷新
|
||||
</button>
|
||||
</div>
|
||||
</AppDialog>
|
||||
</template>
|
||||
@@ -0,0 +1,187 @@
|
||||
<script setup lang="ts">
|
||||
// AppDialog —— 全站统一弹窗封装(reka-ui Dialog 底座)。
|
||||
//
|
||||
// 目的:用 Dialog 原语换取焦点捕获、Esc 关闭、body 滚动锁定、aria 语义与 Portal 渲染,
|
||||
// 同时保持与现有手写弹窗一致的视觉(白色卡片 + 圆角 + 遮罩)。
|
||||
//
|
||||
// 结构说明(与 reka 的约定):
|
||||
// - DialogContent(DismissableLayer)本身不带定位样式,只作为焦点/事件容器;
|
||||
// - 真正的「铺满全屏的定位层」是它内部的 div:pointer-events:none 让点击穿透到底下的
|
||||
// DialogOverlay,从而「点遮罩关闭」才能成立(点在自己身上不算 outside,不会关闭);
|
||||
// - 面板单独 pointer-events:auto,恢复可交互。
|
||||
// 这与项目里 MobileLayout 手写弹窗的 pointer-events 处理方式一致。
|
||||
//
|
||||
// 面板基础样式(w-full / bg-white / flex flex-col / overflow-hidden / max-h-[85vh] / shadow-xl)
|
||||
// 与原手写弹窗一致;**原弹窗没有阴影时**必须在 panelClass 里显式传 shadow-none
|
||||
// (经 tailwind-merge 覆盖,实测 cn('shadow-xl','shadow-none') → 'shadow-none'),
|
||||
// 否则会凭空多出阴影(LeaveView 等页面的面板原本无阴影)。
|
||||
//
|
||||
// 形态(variant):
|
||||
// - sheet(默认):移动端贴底、桌面端居中(≈原 items-end sm:items-center),
|
||||
// 面板默认 rounded-t-2xl sm:rounded-2xl,调用方在 panelClass 里给 sm:max-w-sm;
|
||||
// - center:始终居中(≈原 items-center),调用方在 panelClass 里给宽度与内边距。
|
||||
//
|
||||
// 典型用法:
|
||||
// <AppDialog v-model:open="show" variant="center" z-class="z-[200]"
|
||||
// panel-class="w-[86%] max-w-[340px] p-5" title="发现新版本" :description="desc">
|
||||
// <!-- 面板内容(视觉标题由业务自己渲染,组件内的 Title/Description 仅供读屏) -->
|
||||
// </AppDialog>
|
||||
//
|
||||
// 关闭控制:dismissable=false 时拦截遮罩点击与 Esc(对应「执行中不允许关闭」);
|
||||
// 关闭动作统一经 update:open 冒泡,调用方按需守卫。
|
||||
import {
|
||||
DialogRoot,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from 'reka-ui'
|
||||
import type { FocusOutsideEvent, PointerDownOutsideEvent } from 'reka-ui'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
open: boolean
|
||||
/** sheet=移动端贴底/桌面居中;center=始终居中 */
|
||||
variant?: 'sheet' | 'center'
|
||||
/** 层级类(遮罩与定位层同层,如 z-[60]) */
|
||||
zClass?: string
|
||||
/** 遮罩浓度类(默认 bg-black/40,部分弹窗用 /50) */
|
||||
overlayClass?: string
|
||||
/**
|
||||
* 淡入淡出(默认关)。
|
||||
* 注意:reka-ui 的 Presence 只识别 CSS animation(读 computed animation-name),
|
||||
* **不识别 Vue <Transition> 的 class 过渡**,且 DialogContent 根是 fragment
|
||||
* 无法被 <Transition> 包裹。故原手写弹窗的 `<Transition name="fade">`(0.2s 透明度)
|
||||
* 需由本组件的 CSS animation 承接(见文件末尾 style)。
|
||||
*/
|
||||
fade?: boolean
|
||||
/** 定位层附加类(如 px-6 给居中弹窗留边距) */
|
||||
wrapperClass?: string
|
||||
/** 面板附加类(宽度/内边距/圆角等,经 tailwind-merge 合并) */
|
||||
panelClass?: string
|
||||
/** false 时禁止点遮罩与 Esc 关闭(执行中场景) */
|
||||
dismissable?: boolean
|
||||
/** 读屏标题;视觉标题由业务在默认插槽自渲染 */
|
||||
title?: string
|
||||
/** 读屏描述(缺省回退为标题,避免 aria-describedby 指向不存在的节点) */
|
||||
description?: string
|
||||
}>(),
|
||||
{
|
||||
variant: 'sheet',
|
||||
zClass: 'z-50',
|
||||
overlayClass: 'bg-black/40',
|
||||
wrapperClass: '',
|
||||
panelClass: '',
|
||||
dismissable: true,
|
||||
fade: false,
|
||||
title: '提示',
|
||||
description: '',
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', value: boolean): void
|
||||
}>()
|
||||
|
||||
function onOpenChange(value: boolean) {
|
||||
emit('update:open', value)
|
||||
}
|
||||
|
||||
// reka 以 CustomEvent 派发,preventDefault 即阻止 dismiss(见 DismissableLayer 实现)
|
||||
function onInteractOutside(e: PointerDownOutsideEvent | FocusOutsideEvent) {
|
||||
if (!props.dismissable) e.preventDefault()
|
||||
}
|
||||
|
||||
function onEscapeKeyDown(e: KeyboardEvent) {
|
||||
if (!props.dismissable) e.preventDefault()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot :open="props.open" @update:open="onOpenChange">
|
||||
<DialogPortal>
|
||||
<!-- 遮罩:reka 会自动加 pointer-events:auto 并响应「点遮罩关闭」 -->
|
||||
<DialogOverlay
|
||||
:class="cn('fixed inset-0', props.overlayClass, props.zClass, props.fade && 'app-dialog-fade')"
|
||||
/>
|
||||
|
||||
<!-- 事件/焦点容器:本身不承载布局(零尺寸),只作为焦点/事件容器。
|
||||
fade 类必须挂在这里:它会 forward 到 role="dialog" 那个根节点,也就是
|
||||
Presence 追踪的节点;Presence 靠监听该节点自身的 animationend 判断离场
|
||||
结束,动画挂到子节点上则读不到,会直接卸载(瞬间消失)。
|
||||
fade 时额外加 relative + 同层 z:淡入淡出的 opacity<1 会让本节点形成
|
||||
z-index:0 的层叠上下文,届时内部 fixed 面板被困其中,而 overlay(正 z)
|
||||
会在其后绘制、把面板盖住 0.2s 再突然跳回;加上定位与 zClass 后本节点
|
||||
与 overlay 同级且 DOM 在后 → 始终在上。不加 fade 时保持原样(零改动)。 -->
|
||||
<DialogContent
|
||||
:class="cn('outline-none', props.fade && ['relative', props.zClass, 'app-dialog-fade'])"
|
||||
@interact-outside="onInteractOutside"
|
||||
@escape-key-down="onEscapeKeyDown"
|
||||
>
|
||||
<!-- 定位层:铺满全屏但点击穿透(点空白处落到遮罩上才能关闭) -->
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'fixed inset-0 flex justify-center',
|
||||
props.variant === 'sheet' ? 'items-end sm:items-center' : 'items-center',
|
||||
props.wrapperClass,
|
||||
props.zClass,
|
||||
)
|
||||
"
|
||||
style="pointer-events: none"
|
||||
>
|
||||
<!-- 面板:视觉与手写弹窗一致;pointer-events 恢复以便交互 -->
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'relative w-full bg-white flex flex-col overflow-hidden shadow-xl max-h-[85vh]',
|
||||
props.variant === 'sheet' ? 'rounded-t-2xl sm:rounded-2xl' : 'rounded-2xl',
|
||||
props.panelClass,
|
||||
)
|
||||
"
|
||||
style="pointer-events: auto"
|
||||
>
|
||||
<!-- 读屏标题/描述(视觉标题由业务自渲染,这里不重复显示) -->
|
||||
<DialogTitle class="sr-only">{{ props.title }}</DialogTitle>
|
||||
<DialogDescription class="sr-only">{{ props.description || props.title }}</DialogDescription>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 淡入淡出(fade=true 时启用)。
|
||||
*
|
||||
* 为什么不用 <Transition>:reka-ui 的 Presence 通过读取 computed style 的
|
||||
* animation-name 判断「是否正在动画」,只认 CSS animation;Vue 的 <Transition>
|
||||
* 是 class 过渡,Presence 感知不到,会导致离场动画被直接跳过(瞬间消失)。
|
||||
* 因此这里用 CSS animation + reka 输出的 data-state="open|closed" 实现进出场。
|
||||
* 非 scoped:内容经 DialogPortal 传送到 body,且需同时作用于 overlay 与 content。 */
|
||||
.app-dialog-fade[data-state='open'] {
|
||||
animation: app-dialog-fade-in 0.2s ease;
|
||||
}
|
||||
.app-dialog-fade[data-state='closed'] {
|
||||
animation: app-dialog-fade-out 0.2s ease;
|
||||
}
|
||||
@keyframes app-dialog-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes app-dialog-fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import { CalendarCheck2, CalendarX2, User, Bell, X, CheckCircle, ChevronDown, MessageSquare, Sparkles } from 'lucide-vue-next'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import GuideOverlay from '@/components/GuideOverlay.vue'
|
||||
import * as authApi from '@/api/auth'
|
||||
import * as announceApi from '@/api/announcement'
|
||||
@@ -297,19 +298,15 @@ onUnmounted(() => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ======== 公告面板遮罩 ======== -->
|
||||
<transition name="fade">
|
||||
<div v-if="showAnnPanel" class="fixed inset-0 z-50 bg-black/50" @click="showAnnPanel = false" />
|
||||
</transition>
|
||||
|
||||
<!-- ======== 公告底部面板 ======== -->
|
||||
<transition name="slide-up">
|
||||
<div
|
||||
v-if="showAnnPanel"
|
||||
class="fixed inset-0 z-50 flex items-end sm:items-center justify-center pointer-events-none"
|
||||
<!-- ======== 公告面板(遮罩 + 底部面板) ======== -->
|
||||
<AppDialog
|
||||
:open="showAnnPanel"
|
||||
overlay-class="bg-black/50"
|
||||
panel-class="sm:max-w-sm max-h-[80vh] shadow-2xl"
|
||||
:fade="true"
|
||||
title="通知"
|
||||
@update:open="(v: boolean) => { if (!v) showAnnPanel = false }"
|
||||
>
|
||||
<div class="absolute inset-0 pointer-events-auto" @click="showAnnPanel = false" />
|
||||
<div class="relative w-full sm:max-w-sm max-h-[80vh] bg-white sm:rounded-2xl rounded-t-2xl shadow-2xl flex flex-col overflow-hidden pointer-events-auto">
|
||||
<!-- 拖拽指示条 -->
|
||||
<div class="flex justify-center pt-2.5 pb-1 flex-shrink-0">
|
||||
<div class="w-9 h-1 rounded-full bg-gray-300" />
|
||||
@@ -414,19 +411,20 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 公告详情 Dialog ======== -->
|
||||
<Teleport to="body">
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="showDetail && detailItem"
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center px-6"
|
||||
<AppDialog
|
||||
:open="showDetail && !!detailItem"
|
||||
variant="center"
|
||||
z-class="z-[60]"
|
||||
overlay-class="bg-black/50"
|
||||
wrapper-class="px-6"
|
||||
panel-class="max-w-sm shadow-2xl"
|
||||
:fade="true"
|
||||
:title="detailItem?.title || '公告详情'"
|
||||
@update:open="(v: boolean) => { if (!v) showDetail = false }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/50" @click="showDetail = false" />
|
||||
<div class="relative w-full max-w-sm bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||
<div class="h-1.5 bg-gradient-to-r from-blue-500 to-blue-400" />
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
@@ -434,36 +432,36 @@ onUnmounted(() => {
|
||||
<div class="w-9 h-9 rounded-xl bg-blue-100 flex items-center justify-center">
|
||||
<Bell class="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
<h3 class="text-base font-bold text-gray-900">{{ detailItem.title }}</h3>
|
||||
<h3 class="text-base font-bold text-gray-900">{{ detailItem?.title }}</h3>
|
||||
</div>
|
||||
<button class="w-7 h-7 rounded-full hover:bg-gray-100 flex items-center justify-center flex-shrink-0" @click="showDetail = false">
|
||||
<X class="w-4 h-4 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="text-[10px] text-gray-400">{{ new Date(detailItem.createdAt).toLocaleString('zh-CN') }}</span>
|
||||
<span class="text-[10px] text-gray-400">{{ detailItem ? new Date(detailItem.createdAt).toLocaleString('zh-CN') : '' }}</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-4 max-h-[50vh] overflow-y-auto">
|
||||
<p class="text-sm text-gray-600 leading-relaxed whitespace-pre-wrap">{{ detailItem.content }}</p>
|
||||
<p class="text-sm text-gray-600 leading-relaxed whitespace-pre-wrap">{{ detailItem?.content }}</p>
|
||||
</div>
|
||||
<Button class="mt-4 w-full h-11 text-sm font-medium rounded-xl" @click="showDetail = false">
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 强制弹出公告 Dialog ======== -->
|
||||
<Teleport to="body">
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="currentForcePopup"
|
||||
class="fixed inset-0 z-[60] flex items-center justify-center px-6"
|
||||
<AppDialog
|
||||
:open="!!currentForcePopup"
|
||||
variant="center"
|
||||
z-class="z-[60]"
|
||||
overlay-class="bg-black/50"
|
||||
wrapper-class="px-6"
|
||||
panel-class="max-w-sm shadow-2xl"
|
||||
:fade="true"
|
||||
:title="currentForcePopup?.title || '公告'"
|
||||
@update:open="(v: boolean) => { if (!v) handleForcePopupDismiss() }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/50" @click="handleForcePopupDismiss" />
|
||||
<div class="relative w-full max-w-sm bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||
<!-- 顶部装饰条 -->
|
||||
<div class="h-1.5 bg-gradient-to-r from-blue-500 to-blue-400" />
|
||||
<div class="p-6">
|
||||
@@ -471,19 +469,16 @@ onUnmounted(() => {
|
||||
<div class="w-9 h-9 rounded-xl bg-blue-100 flex items-center justify-center">
|
||||
<Bell class="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
<h3 class="text-base font-bold text-gray-900">{{ currentForcePopup.title }}</h3>
|
||||
<h3 class="text-base font-bold text-gray-900">{{ currentForcePopup?.title }}</h3>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-4 mb-4">
|
||||
<p class="text-sm text-gray-600 leading-relaxed whitespace-pre-wrap">{{ currentForcePopup.content }}</p>
|
||||
<p class="text-sm text-gray-600 leading-relaxed whitespace-pre-wrap">{{ currentForcePopup?.content }}</p>
|
||||
</div>
|
||||
<Button class="w-full h-11 text-sm font-medium rounded-xl" @click="handleForcePopupDismiss">
|
||||
我知道了
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 新手引导入口(全局:所有布局内页面显示;引导展示期间隐藏) ======== -->
|
||||
<button
|
||||
@@ -507,24 +502,6 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slide-up-enter-active,
|
||||
.slide-up-leave-active {
|
||||
transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
.slide-up-enter-from,
|
||||
.slide-up-leave-to {
|
||||
transition: translateY(100%);
|
||||
}
|
||||
|
||||
/* 引导入口气泡:上下浮动跳动,吸引注意但不刺眼 */
|
||||
@keyframes guide-bob {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { } from '@/components/ui/card'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import * as adminApi from '@/api/admin'
|
||||
|
||||
@@ -294,9 +294,13 @@ async function handleToggleStatus(agent: adminApi.AgentUser) {
|
||||
</div>
|
||||
|
||||
<!-- 创建代理弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showCreateModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40" @click.self="showCreateModal = false">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-md mx-4 p-6">
|
||||
<AppDialog
|
||||
:open="showCreateModal"
|
||||
variant="center"
|
||||
panel-class="rounded-xl max-w-md mx-4 p-6"
|
||||
title="创建下级代理"
|
||||
@update:open="(v: boolean) => { if (!v) showCreateModal = false }"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900">创建下级代理</h2>
|
||||
<button class="text-gray-400 hover:text-gray-600" @click="showCreateModal = false">
|
||||
@@ -330,14 +334,16 @@ async function handleToggleStatus(agent: adminApi.AgentUser) {
|
||||
创建
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- 充值弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showRechargeModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40" @click.self="showRechargeModal = false">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-sm mx-4 p-6">
|
||||
<AppDialog
|
||||
:open="showRechargeModal"
|
||||
variant="center"
|
||||
panel-class="rounded-xl max-w-sm mx-4 p-6"
|
||||
title="充值"
|
||||
@update:open="(v: boolean) => { if (!v) showRechargeModal = false }"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
充值 - {{ rechargeTarget?.username }}
|
||||
@@ -360,14 +366,16 @@ async function handleToggleStatus(agent: adminApi.AgentUser) {
|
||||
充值
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- 修改费率弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showRateModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40" @click.self="showRateModal = false">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-sm mx-4 p-6">
|
||||
<AppDialog
|
||||
:open="showRateModal"
|
||||
variant="center"
|
||||
panel-class="rounded-xl max-w-sm mx-4 p-6"
|
||||
title="修改费率"
|
||||
@update:open="(v: boolean) => { if (!v) showRateModal = false }"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
修改费率 - {{ rateTarget?.username }}
|
||||
@@ -391,14 +399,16 @@ async function handleToggleStatus(agent: adminApi.AgentUser) {
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- 修改密码弹窗 (admin only) -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showPasswordModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40" @click.self="showPasswordModal = false">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-sm mx-4 p-6">
|
||||
<AppDialog
|
||||
:open="showPasswordModal"
|
||||
variant="center"
|
||||
panel-class="rounded-xl max-w-sm mx-4 p-6"
|
||||
title="修改密码"
|
||||
@update:open="(v: boolean) => { if (!v) showPasswordModal = false }"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-gray-900">
|
||||
修改密码 - {{ passwordTarget?.username }}
|
||||
@@ -435,8 +445,6 @@ async function handleToggleStatus(agent: adminApi.AgentUser) {
|
||||
确认修改
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Megaphone, Plus, Pencil, Trash2, X, Bell, Users, User, Bot, Search } fr
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import * as adminApi from '@/api/admin'
|
||||
|
||||
@@ -329,13 +330,13 @@ function scopeLabel(item: AnnouncementEx) {
|
||||
</div>
|
||||
|
||||
<!-- 创建弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showCreateModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
<AppDialog
|
||||
:open="showCreateModal"
|
||||
variant="center"
|
||||
panel-class="w-[85%] sm:max-w-md"
|
||||
:title="editingId ? '编辑公告' : '新建公告'"
|
||||
@update:open="(v: boolean) => { showCreateModal = v }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40" @click="showCreateModal = false" />
|
||||
<div class="relative w-[85%] sm:max-w-md bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="flex items-center justify-between px-5 pt-5 pb-3">
|
||||
<h3 class="text-base font-semibold text-gray-900">{{ editingId ? '编辑公告' : '新建公告' }}</h3>
|
||||
<button class="w-7 h-7 rounded-full bg-gray-100 flex items-center justify-center text-gray-400 hover:text-gray-600" @click="showCreateModal = false">
|
||||
@@ -450,8 +451,6 @@ function scopeLabel(item: AnnouncementEx) {
|
||||
{{ creating ? '保存中...' : (editingId ? '保存修改' : '发布公告') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import * as adminApi from '@/api/admin'
|
||||
@@ -182,8 +183,8 @@ async function copyCode(code: string, index: number) {
|
||||
|
||||
async function copyResultCode(code: string, index: number) {
|
||||
if (await copyToClipboard(code)) {
|
||||
resultCopiedIdx.value = index
|
||||
setTimeout(() => { resultCopiedIdx.value = null }, 2000)
|
||||
copiedResultIdx.value = index
|
||||
setTimeout(() => { copiedResultIdx.value = null }, 2000)
|
||||
message.success('已复制')
|
||||
} else {
|
||||
message.error('复制失败,请长按 CDK 码手动复制')
|
||||
@@ -434,11 +435,13 @@ function statusClass(s: string) {
|
||||
</div>
|
||||
|
||||
<!-- 生成CDK弹窗(form / result 两视图) -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showGenerateModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40" @click.self="modalView === 'form' ? (showGenerateModal = false) : null">
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-xl w-full mx-4 p-6 flex flex-col"
|
||||
:class="modalView === 'result' ? 'max-w-lg max-h-[80vh]' : 'max-w-sm'"
|
||||
<AppDialog
|
||||
:open="showGenerateModal"
|
||||
variant="center"
|
||||
:dismissable="modalView === 'form'"
|
||||
:panel-class="modalView === 'result' ? 'rounded-xl mx-4 p-6 max-w-lg max-h-[80vh]' : 'rounded-xl mx-4 p-6 max-w-sm'"
|
||||
title="生成CDK"
|
||||
@update:open="(v: boolean) => { if (!v && modalView === 'form') showGenerateModal = false }"
|
||||
>
|
||||
<!-- 配置视图 -->
|
||||
<template v-if="modalView === 'form'">
|
||||
@@ -566,14 +569,16 @@ function statusClass(s: string) {
|
||||
提示:CDK 按行分隔,可直接粘贴到表格/文档批量分发
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- 禁用/恢复确认弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showStatusModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40" @click.self="showStatusModal = false">
|
||||
<div class="bg-white rounded-xl shadow-xl w-full max-w-sm mx-4 p-6">
|
||||
<AppDialog
|
||||
:open="showStatusModal"
|
||||
variant="center"
|
||||
panel-class="rounded-xl max-w-sm mx-4 p-6"
|
||||
:title="statusText"
|
||||
@update:open="(v: boolean) => { if (!v) showStatusModal = false }"
|
||||
>
|
||||
<div class="flex items-center gap-2.5 mb-3">
|
||||
<div
|
||||
class="w-9 h-9 rounded-xl flex items-center justify-center"
|
||||
@@ -607,8 +612,6 @@ function statusClass(s: string) {
|
||||
{{ statusAction === 'disable' ? '确认禁用' : '确认恢复' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from 'lucide-vue-next'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import type { AgentInfo } from '@/api/admin'
|
||||
import * as announceApi from '@/api/announcement'
|
||||
import type { AnnouncementItem } from '@/api/announcement'
|
||||
@@ -328,11 +329,17 @@ function handleLogout() {
|
||||
</transition>
|
||||
|
||||
<!-- 代理端公告详情弹窗 -->
|
||||
<Teleport to="body">
|
||||
<transition name="fade">
|
||||
<div v-if="showDetail && detailItem" class="fixed inset-0 z-[60] flex items-center justify-center px-4">
|
||||
<div class="absolute inset-0 bg-black/50" @click="showDetail = false" />
|
||||
<div class="relative w-[85%] sm:max-w-sm bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||
<AppDialog
|
||||
:open="showDetail && !!detailItem"
|
||||
variant="center"
|
||||
z-class="z-[60]"
|
||||
wrapper-class="px-4"
|
||||
overlay-class="bg-black/50"
|
||||
panel-class="w-[85%] sm:max-w-sm shadow-2xl"
|
||||
:fade="true"
|
||||
title="公告详情"
|
||||
@update:open="(v: boolean) => { if (!v) showDetail = false }"
|
||||
>
|
||||
<div class="h-1.5 bg-gradient-to-r from-blue-500 to-blue-400" />
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
@@ -340,43 +347,42 @@ function handleLogout() {
|
||||
<div class="w-9 h-9 rounded-xl bg-blue-100 flex items-center justify-center">
|
||||
<Bell class="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
<h3 class="text-base font-bold text-gray-900">{{ detailItem.title }}</h3>
|
||||
<h3 class="text-base font-bold text-gray-900">{{ detailItem?.title }}</h3>
|
||||
</div>
|
||||
<button class="w-7 h-7 rounded-full hover:bg-gray-100 flex items-center justify-center flex-shrink-0" @click="showDetail = false">
|
||||
<X class="w-4 h-4 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="text-[10px] text-gray-400">{{ new Date(detailItem.createdAt).toLocaleString('zh-CN') }}</span>
|
||||
<span class="text-[10px] text-gray-400">{{ detailItem ? new Date(detailItem.createdAt).toLocaleString('zh-CN') : '' }}</span>
|
||||
</div>
|
||||
<div class="bg-gray-50 rounded-xl p-4 max-h-[50vh] overflow-y-auto">
|
||||
<p class="text-sm text-gray-600 leading-relaxed whitespace-pre-wrap">{{ detailItem.content }}</p>
|
||||
<p class="text-sm text-gray-600 leading-relaxed whitespace-pre-wrap">{{ detailItem?.content }}</p>
|
||||
</div>
|
||||
<Button class="mt-4 w-full h-11 text-sm font-medium rounded-xl" @click="showDetail = false">关闭</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- 强制弹出公告 Dialog -->
|
||||
<Teleport to="body">
|
||||
<transition name="fade">
|
||||
<div v-if="currentForcePopup" class="fixed inset-0 z-[60] flex items-center justify-center px-4">
|
||||
<div class="absolute inset-0 bg-black/40" @click="dismissForcePopup" />
|
||||
<div class="relative w-[85%] sm:max-w-sm bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<AppDialog
|
||||
:open="!!currentForcePopup"
|
||||
variant="center"
|
||||
z-class="z-[60]"
|
||||
wrapper-class="px-4"
|
||||
panel-class="w-[85%] sm:max-w-sm"
|
||||
:fade="true"
|
||||
title="强制公告"
|
||||
@update:open="(v: boolean) => { if (!v) dismissForcePopup() }"
|
||||
>
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<Bell class="w-5 h-5 text-blue-500" />
|
||||
<h3 class="text-base font-semibold text-gray-900">{{ currentForcePopup.title }}</h3>
|
||||
<h3 class="text-base font-semibold text-gray-900">{{ currentForcePopup?.title }}</h3>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 leading-relaxed mt-3 whitespace-pre-wrap">{{ currentForcePopup.content }}</p>
|
||||
<p class="text-sm text-gray-600 leading-relaxed mt-3 whitespace-pre-wrap">{{ currentForcePopup?.content }}</p>
|
||||
<Button class="mt-5 w-full h-10 text-sm font-medium" @click="dismissForcePopup">我知道了</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted } from 'vue'
|
||||
import { Shield, RefreshCw, CheckCircle, XCircle, Search, Eye, EyeOff, Copy } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import { copyToClipboard } from '@/utils/clipboard'
|
||||
import api from '@/api/index'
|
||||
@@ -383,20 +384,22 @@ async function copyText(text: string) {
|
||||
</div>
|
||||
|
||||
<!-- 拒绝理由弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="rejectTarget"
|
||||
class="fixed inset-0 z-50 bg-black/40 flex items-center justify-center px-4"
|
||||
@click.self="closeRejectModal"
|
||||
<AppDialog
|
||||
:open="!!rejectTarget"
|
||||
variant="center"
|
||||
wrapper-class="px-4"
|
||||
panel-class="max-w-md p-6"
|
||||
:dismissable="!rejectSubmitting"
|
||||
title="拒绝适配申请"
|
||||
@update:open="(v: boolean) => { if (!v) closeRejectModal() }"
|
||||
>
|
||||
<div class="bg-white rounded-2xl w-full max-w-md p-6 shadow-xl">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-9 h-9 rounded-full bg-red-50 flex items-center justify-center">
|
||||
<XCircle class="w-5 h-5 text-red-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-bold text-gray-900">拒绝适配申请</h3>
|
||||
<p class="text-xs text-gray-400">{{ rejectTarget.schoolName }}</p>
|
||||
<p class="text-xs text-gray-400">{{ rejectTarget?.schoolName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="block text-sm text-gray-600 mb-1.5">拒绝理由 <span class="text-red-500">*</span></label>
|
||||
@@ -417,8 +420,6 @@ async function copyText(text: string) {
|
||||
{{ rejectSubmitting ? '提交中...' : '确认拒绝' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import * as adminApi from '@/api/admin'
|
||||
|
||||
@@ -613,11 +613,15 @@ function todayStatusLabel(s: string) {
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="showDetail" class="fixed inset-0 z-50 flex items-center justify-center px-4">
|
||||
<div class="absolute inset-0 bg-black/40" @click="showDetail = false" />
|
||||
<div class="relative w-[90%] sm:max-w-lg bg-white rounded-2xl shadow-xl overflow-hidden max-h-[90vh] flex flex-col">
|
||||
<AppDialog
|
||||
:open="showDetail"
|
||||
variant="center"
|
||||
wrapper-class="px-4"
|
||||
panel-class="w-[90%] sm:max-w-lg max-h-[90vh]"
|
||||
:fade="true"
|
||||
title="用户详情"
|
||||
@update:open="(v: boolean) => { showDetail = v }"
|
||||
>
|
||||
<div class="h-1.5 bg-gradient-to-r from-green-500 to-green-400" />
|
||||
<div class="flex items-center justify-between px-5 pt-5 pb-3">
|
||||
<div class="flex items-center gap-2.5">
|
||||
@@ -733,17 +737,18 @@ function todayStatusLabel(s: string) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- 禁用弹窗 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="showDisableModal" class="fixed inset-0 z-50 flex items-center justify-center px-4">
|
||||
<div class="absolute inset-0 bg-black/40" @click="showDisableModal = false" />
|
||||
<div class="relative w-[85%] sm:max-w-sm bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<AppDialog
|
||||
:open="showDisableModal"
|
||||
variant="center"
|
||||
wrapper-class="px-4"
|
||||
panel-class="w-[85%] sm:max-w-sm"
|
||||
:fade="true"
|
||||
title="禁用用户"
|
||||
@update:open="(v: boolean) => { showDisableModal = v }"
|
||||
>
|
||||
<div class="h-1.5 bg-gradient-to-r from-red-500 to-orange-400" />
|
||||
<div class="p-6 space-y-4">
|
||||
<div class="flex items-center gap-2.5">
|
||||
@@ -780,17 +785,18 @@ function todayStatusLabel(s: string) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- 当天执行日志弹窗 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="showLogsModal" class="fixed inset-0 z-50 flex items-center justify-center px-4">
|
||||
<div class="absolute inset-0 bg-black/40" @click="showLogsModal = false" />
|
||||
<div class="relative w-full sm:max-w-3xl bg-white rounded-2xl shadow-xl overflow-hidden flex flex-col max-h-[85vh]">
|
||||
<AppDialog
|
||||
:open="showLogsModal"
|
||||
variant="center"
|
||||
wrapper-class="px-4"
|
||||
panel-class="sm:max-w-3xl"
|
||||
:fade="true"
|
||||
title="执行日志明细"
|
||||
@update:open="(v: boolean) => { showLogsModal = v }"
|
||||
>
|
||||
<div class="h-1.5 bg-gradient-to-r from-blue-500 to-cyan-400 flex-shrink-0" />
|
||||
|
||||
<!-- 头部 -->
|
||||
@@ -932,20 +938,6 @@ function todayStatusLabel(s: string) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { CheckCircle, Clock, Bell, Calendar, Plus, Play, Trash2, ListTodo, MapPin, LoaderCircle, Edit3, Upload, Pencil, X, Gift, Smartphone, Star } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import * as authApi from '@/api/auth'
|
||||
import { CPD_PASSWORD_ERROR } from '@/api'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
@@ -1065,14 +1066,12 @@ function getGPSLocation(source: string) {
|
||||
</button>
|
||||
|
||||
<!-- ======== 创建流程 - 步骤弹窗 ======== -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showStep"
|
||||
class="fixed inset-0 z-50 flex items-end sm:items-center justify-center"
|
||||
<AppDialog
|
||||
:open="showStep"
|
||||
panel-class="shadow-none sm:max-w-sm"
|
||||
title="设置步骤"
|
||||
@update:open="(v: boolean) => { if (!v) showStep = false }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40" @click="showStep = false" />
|
||||
|
||||
<div class="relative w-full sm:max-w-sm max-h-[85vh] bg-white rounded-t-2xl sm:rounded-2xl overflow-hidden flex flex-col">
|
||||
<!-- 标题栏 -->
|
||||
<div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between shrink-0">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -1271,19 +1270,15 @@ function getGPSLocation(source: string) {
|
||||
{{ savingForm ? '保存中...' : '确认保存' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 编辑弹窗 ======== -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showEditModal"
|
||||
class="fixed inset-0 z-50 flex items-end sm:items-center justify-center"
|
||||
<AppDialog
|
||||
:open="showEditModal"
|
||||
panel-class="shadow-none sm:max-w-sm"
|
||||
title="编辑表单"
|
||||
@update:open="(v: boolean) => { if (!v) showEditModal = false }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40" @click="showEditModal = false" />
|
||||
|
||||
<div class="relative w-full sm:max-w-sm max-h-[85vh] bg-white rounded-t-2xl sm:rounded-2xl overflow-hidden flex flex-col">
|
||||
<!-- 标题栏 -->
|
||||
<div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between shrink-0">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -1425,18 +1420,16 @@ function getGPSLocation(source: string) {
|
||||
{{ savingEdit ? '保存中...' : '保存表单' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 收藏本站指引弹窗 ======== -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showBookmarkDialog"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
<AppDialog
|
||||
:open="showBookmarkDialog"
|
||||
variant="center"
|
||||
panel-class="w-[85%] sm:max-w-sm"
|
||||
title="收藏本站"
|
||||
@update:open="(v: boolean) => { if (!v) showBookmarkDialog = false }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40" @click="showBookmarkDialog = false" />
|
||||
<div class="relative w-[85%] sm:max-w-sm bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center shrink-0">
|
||||
@@ -1476,18 +1469,16 @@ function getGPSLocation(source: string) {
|
||||
我知道了
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 已打卡再次执行确认弹窗 ======== -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showConfirmExecDialog"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
<AppDialog
|
||||
:open="showConfirmExecDialog"
|
||||
variant="center"
|
||||
panel-class="w-[85%] sm:max-w-sm"
|
||||
title="确认再次打卡"
|
||||
@update:open="(v: boolean) => { if (!v) showConfirmExecDialog = false }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40" @click="showConfirmExecDialog = false" />
|
||||
<div class="relative w-[85%] sm:max-w-sm bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="p-6 text-center">
|
||||
<div class="w-14 h-14 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
|
||||
<CheckCircle class="w-7 h-7 text-green-500" />
|
||||
@@ -1512,18 +1503,16 @@ function getGPSLocation(source: string) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 无可用签到次数弹窗 ======== -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showNoRunsDialog"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
<AppDialog
|
||||
:open="showNoRunsDialog"
|
||||
variant="center"
|
||||
panel-class="w-[85%] sm:max-w-sm"
|
||||
title="签到次数不足"
|
||||
@update:open="(v: boolean) => { if (!v) showNoRunsDialog = false }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40" @click="showNoRunsDialog = false" />
|
||||
<div class="relative w-[85%] sm:max-w-sm bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<!-- 内容 -->
|
||||
<div class="p-6 text-center">
|
||||
<div class="w-14 h-14 rounded-full bg-amber-100 flex items-center justify-center mx-auto mb-4">
|
||||
@@ -1550,18 +1539,16 @@ function getGPSLocation(source: string) {
|
||||
我知道了
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 未绑定设备码弹窗(新建任务入口拦截) ======== -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showBindDeviceDialog"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
<AppDialog
|
||||
:open="showBindDeviceDialog"
|
||||
variant="center"
|
||||
panel-class="w-[85%] sm:max-w-sm"
|
||||
title="绑定设备"
|
||||
@update:open="(v: boolean) => { if (!v) showBindDeviceDialog = false }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40" @click="showBindDeviceDialog = false" />
|
||||
<div class="relative w-[85%] sm:max-w-sm bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="p-6 text-center">
|
||||
<div class="w-14 h-14 rounded-full bg-indigo-100 flex items-center justify-center mx-auto mb-4">
|
||||
<Smartphone class="w-7 h-7 text-indigo-500" />
|
||||
@@ -1582,18 +1569,16 @@ function getGPSLocation(source: string) {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 操作频繁弹窗(限流) ======== -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showRateLimitDialog"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
<AppDialog
|
||||
:open="showRateLimitDialog"
|
||||
variant="center"
|
||||
panel-class="w-[85%] sm:max-w-sm"
|
||||
title="操作频繁"
|
||||
@update:open="(v: boolean) => { if (!v) showRateLimitDialog = false }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40" @click="showRateLimitDialog = false" />
|
||||
<div class="relative w-[85%] sm:max-w-sm bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="p-6 text-center">
|
||||
<div class="w-14 h-14 rounded-full bg-orange-100 flex items-center justify-center mx-auto mb-4">
|
||||
<Clock class="w-7 h-7 text-orange-500" />
|
||||
@@ -1609,18 +1594,16 @@ function getGPSLocation(source: string) {
|
||||
我知道了
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 设备频繁切换弹窗 ======== -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showDeviceSwitchDialog"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
<AppDialog
|
||||
:open="showDeviceSwitchDialog"
|
||||
variant="center"
|
||||
panel-class="w-[85%] sm:max-w-sm"
|
||||
title="设备频繁切换"
|
||||
@update:open="(v: boolean) => { if (!v) showDeviceSwitchDialog = false }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40" @click="showDeviceSwitchDialog = false" />
|
||||
<div class="relative w-[85%] sm:max-w-sm bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="p-6 text-center">
|
||||
<div class="w-14 h-14 rounded-full bg-orange-100 flex items-center justify-center mx-auto mb-4">
|
||||
<Smartphone class="w-7 h-7 text-orange-500" />
|
||||
@@ -1639,18 +1622,17 @@ function getGPSLocation(source: string) {
|
||||
前往个人中心
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 登录凭证过期弹窗(cpdaily 密码错误) ======== -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="showReLoginDialog"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
<AppDialog
|
||||
:open="showReLoginDialog"
|
||||
variant="center"
|
||||
panel-class="w-[85%] sm:max-w-sm"
|
||||
:dismissable="false"
|
||||
title="重新登录"
|
||||
@update:open="(v: boolean) => { if (!v) showReLoginDialog = false }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/40" />
|
||||
<div class="relative w-[85%] sm:max-w-sm bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<div class="p-6 text-center">
|
||||
<div class="w-14 h-14 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-4">
|
||||
<X class="w-7 h-7 text-red-500" />
|
||||
@@ -1666,9 +1648,7 @@ function getGPSLocation(source: string) {
|
||||
重新登录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -84,10 +84,14 @@
|
||||
</div>
|
||||
|
||||
<!-- ======== 待销假列表弹窗 ======== -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showPendingModal" class="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
||||
<div class="absolute inset-0 bg-black/40" @click="closePendingModal"></div>
|
||||
<div class="relative w-full sm:max-w-sm max-h-[85vh] bg-white rounded-t-2xl sm:rounded-2xl overflow-hidden flex flex-col">
|
||||
<AppDialog
|
||||
:open="showPendingModal"
|
||||
variant="sheet"
|
||||
panel-class="shadow-none sm:max-w-sm"
|
||||
:dismissable="true"
|
||||
title="选择请假记录"
|
||||
@update:open="onPendingOpenChange"
|
||||
>
|
||||
<!-- 标题栏 -->
|
||||
<div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between shrink-0">
|
||||
<span class="text-sm font-semibold text-gray-900">选择请假记录</span>
|
||||
@@ -128,15 +132,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 销假表单弹窗 ======== -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showFormModal" class="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
||||
<div class="absolute inset-0 bg-black/40" @click="closeFormModal"></div>
|
||||
<div class="relative w-full sm:max-w-sm max-h-[85vh] bg-white rounded-t-2xl sm:rounded-2xl overflow-hidden flex flex-col">
|
||||
<AppDialog
|
||||
:open="showFormModal"
|
||||
variant="sheet"
|
||||
panel-class="shadow-none sm:max-w-sm"
|
||||
:dismissable="!executing"
|
||||
title="确认销假信息"
|
||||
@update:open="onFormOpenChange"
|
||||
>
|
||||
<!-- 标题栏 -->
|
||||
<div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between shrink-0">
|
||||
<span class="text-sm font-semibold text-gray-900">确认销假信息</span>
|
||||
@@ -308,14 +314,16 @@
|
||||
{{ executing ? '销假中...' : '立即销假' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
<!-- ======== 请假申请弹窗 ======== -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showApplyModal" class="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
||||
<div class="absolute inset-0 bg-black/40" @click="closeApplyModal"></div>
|
||||
<div class="relative w-full sm:max-w-sm max-h-[85vh] bg-white rounded-t-2xl sm:rounded-2xl overflow-hidden flex flex-col">
|
||||
<AppDialog
|
||||
:open="showApplyModal"
|
||||
variant="sheet"
|
||||
panel-class="shadow-none sm:max-w-sm"
|
||||
:dismissable="!applying"
|
||||
title="请假申请"
|
||||
@update:open="onApplyOpenChange"
|
||||
>
|
||||
<!-- 标题栏 -->
|
||||
<div class="px-4 py-3 border-b border-gray-100 flex items-center justify-between shrink-0">
|
||||
<span class="text-sm font-semibold text-gray-900">请假申请</span>
|
||||
@@ -599,9 +607,7 @@
|
||||
{{ applying ? '提交中...' : '提交请假' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
<!-- ======== 无法请/销假:右侧贴边窄竖条(样式同打卡页「收藏本站」),点击跳转工单页 ======== -->
|
||||
<button
|
||||
class="fixed right-0 bottom-32 z-40 flex flex-col items-center gap-1.5 rounded-l-md bg-rose-500/95 py-3 px-1.5 text-white text-xs font-medium shadow-lg shadow-rose-500/30 hover:bg-rose-600 active:scale-95 transition-all"
|
||||
@@ -620,6 +626,7 @@ import { CalendarCheck, CalendarPlus, LoaderCircle, Trash2, X, MapPin, Upload, M
|
||||
import * as leaveApi from '@/api/leave'
|
||||
import type { LeaveItem, LeaveReportPlace, LeaveType } from '@/api/leave'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
|
||||
const { message } = useMessage()
|
||||
const router = useRouter()
|
||||
@@ -669,6 +676,13 @@ function closePendingModal() {
|
||||
showPendingModal.value = false
|
||||
}
|
||||
|
||||
// AppDialog 的 open 变更统一入口(点遮罩 / Esc / 关闭按钮都经此);
|
||||
// dismissable 已按「执行中」状态在模板上关闭,故此处直接复用原关闭逻辑。
|
||||
function onPendingOpenChange(value: boolean) {
|
||||
if (value) showPendingModal.value = true
|
||||
else closePendingModal()
|
||||
}
|
||||
|
||||
async function loadPendingLeaves() {
|
||||
loadingLeaves.value = true
|
||||
try {
|
||||
@@ -778,6 +792,13 @@ function closeFormModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// 销假弹窗 open 变更入口;dismissable 由模板按 executing 控制,
|
||||
// 此处复用 closeFormModal 的守卫(执行中仍会拒绝关闭)作双保险。
|
||||
function onFormOpenChange(value: boolean) {
|
||||
if (value) showFormModal.value = true
|
||||
else closeFormModal()
|
||||
}
|
||||
|
||||
// ======== 销假图片上传 → 浏览器直传到今日校园 OBS(不经过服务器中转),taskType=leave ========
|
||||
const uploadingImage = ref(false)
|
||||
|
||||
@@ -1047,6 +1068,12 @@ function closeApplyModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// 请假申请弹窗 open 变更入口;同样以 closeApplyModal 守卫兜底(提交中不允许关闭)。
|
||||
function onApplyOpenChange(value: boolean) {
|
||||
if (value) showApplyModal.value = true
|
||||
else closeApplyModal()
|
||||
}
|
||||
|
||||
async function loadLeaveTypes() {
|
||||
loadingTypes.value = true
|
||||
try {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Search, User, Lock, Eye, EyeOff, RefreshCw, LogIn, GraduationCap, Loade
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import * as authApi from '@/api/auth'
|
||||
import * as ticketApi from '@/api/ticket'
|
||||
@@ -600,14 +601,16 @@ async function handleReportSubmit() {
|
||||
</div>
|
||||
|
||||
<!-- ======== 多因子认证(MFA)Dialog:选择验证方式 → 输入验证码 ======== -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="showMFADialog"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center px-6"
|
||||
<AppDialog
|
||||
:open="showMFADialog"
|
||||
variant="center"
|
||||
wrapper-class="px-6"
|
||||
overlay-class="bg-black/50"
|
||||
panel-class="max-w-sm shadow-2xl"
|
||||
:fade="true"
|
||||
title="多因子认证"
|
||||
@update:open="(v: boolean) => { if (!v) closeMFADialog() }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/50" @click="closeMFADialog" />
|
||||
<div class="relative w-full max-w-sm bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||
<!-- 顶部装饰条 -->
|
||||
<div class="h-1.5 bg-gradient-to-r from-blue-500 to-cyan-400" />
|
||||
<div class="p-6">
|
||||
@@ -721,20 +724,19 @@ async function handleReportSubmit() {
|
||||
重新登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 登录失败 Dialog ======== -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="showLoginFailDialog"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center px-6"
|
||||
<AppDialog
|
||||
:open="showLoginFailDialog"
|
||||
variant="center"
|
||||
wrapper-class="px-6"
|
||||
overlay-class="bg-black/50"
|
||||
panel-class="max-w-sm shadow-2xl"
|
||||
:fade="true"
|
||||
title="登录失败"
|
||||
@update:open="(v: boolean) => { if (!v) showLoginFailDialog = false }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/50" @click="showLoginFailDialog = false" />
|
||||
<div class="relative w-full max-w-sm bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||
<!-- 顶部装饰条 -->
|
||||
<div class="h-1.5 bg-gradient-to-r from-red-500 to-orange-400" />
|
||||
<div class="p-6">
|
||||
@@ -788,20 +790,19 @@ async function handleReportSubmit() {
|
||||
我知道了
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- ======== 提交工单 Dialog ======== -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="showReportDialog"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center px-6"
|
||||
<AppDialog
|
||||
:open="showReportDialog"
|
||||
variant="center"
|
||||
wrapper-class="px-6"
|
||||
overlay-class="bg-black/50"
|
||||
panel-class="max-w-sm shadow-2xl"
|
||||
:fade="true"
|
||||
title="提交工单"
|
||||
@update:open="(v: boolean) => { if (!v) closeReportDialog() }"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/50" @click="closeReportDialog" />
|
||||
<div class="relative w-full max-w-sm bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||
<!-- 顶部装饰条 -->
|
||||
<div class="h-1.5 bg-gradient-to-r from-amber-500 to-orange-400" />
|
||||
<div class="p-6">
|
||||
@@ -900,9 +901,6 @@ async function handleReportSubmit() {
|
||||
<template v-else>提交</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from 'vue-router'
|
||||
import { LogOut, RefreshCw, ClipboardCheck, Gift, Share2, X, MessageSquare, Dices, TriangleAlert } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import * as authApi from '@/api/auth'
|
||||
import type { UserInfo, MyInviteInfo } from '@/api/auth'
|
||||
@@ -478,10 +479,15 @@ async function handleRedeemInvite() {
|
||||
</div>
|
||||
|
||||
<!-- 兑换弹窗遮罩 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="showRedeemModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4" @click.self="closeRedeemModal">
|
||||
<div class="bg-white rounded-2xl shadow-xl w-full max-w-sm overflow-hidden">
|
||||
<AppDialog
|
||||
:open="showRedeemModal"
|
||||
variant="center"
|
||||
wrapper-class="px-4"
|
||||
panel-class="max-w-sm"
|
||||
:fade="true"
|
||||
:title="redeemKind === 'vip' ? '续费 VIP' : '兑换签到次数'"
|
||||
@update:open="(v: boolean) => { if (!v) closeRedeemModal() }"
|
||||
>
|
||||
<!-- 弹窗头部 -->
|
||||
<div class="flex items-center justify-between px-5 pt-5 pb-2">
|
||||
<h3 class="text-base font-semibold text-gray-900">
|
||||
@@ -547,15 +553,17 @@ async function handleRedeemInvite() {
|
||||
{{ redeeming ? '兑换中...' : '立即兑换' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
|
||||
<!-- 随机绑定设备码:连续两次风险确认弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="randBindStep > 0" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4" @click.self="closeRandomBind">
|
||||
<div class="bg-white rounded-2xl shadow-xl w-full max-w-sm overflow-hidden">
|
||||
<AppDialog
|
||||
:open="randBindStep > 0"
|
||||
variant="center"
|
||||
wrapper-class="px-4"
|
||||
panel-class="max-w-sm"
|
||||
title="随机绑定"
|
||||
@update:open="(v: boolean) => { if (!v) closeRandomBind() }"
|
||||
>
|
||||
<!-- 第一次确认 -->
|
||||
<template v-if="randBindStep === 1">
|
||||
<div class="px-5 pt-5 pb-2 flex items-center justify-between">
|
||||
@@ -608,22 +616,11 @@ async function handleRedeemInvite() {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* VIP 徽标:金色渐变 + 反光条扫过动画
|
||||
注意:不可设置 position,徽标由模板上的 Tailwind absolute 类控制浮层定位,
|
||||
若此处声明 position:relative 会覆盖 absolute,导致徽标挤进头像布局使其变形 */
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from 'vue-router'
|
||||
import { MessageSquare, Plus, ArrowLeft, Send, X, Trash2 } from 'lucide-vue-next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import { useMessage } from '@/composables/useMessage'
|
||||
import * as ticketApi from '@/api/ticket'
|
||||
import type { TicketListItem } from '@/api/ticket'
|
||||
@@ -170,10 +171,13 @@ async function handleDelete(t: TicketListItem, e: Event) {
|
||||
</div>
|
||||
|
||||
<!-- 新建工单弹窗 -->
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="showCreate" class="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/40" @click.self="closeCreate">
|
||||
<div class="bg-white rounded-t-2xl sm:rounded-2xl shadow-xl w-full max-w-sm overflow-hidden max-h-[85vh] flex flex-col">
|
||||
<AppDialog
|
||||
:open="showCreate"
|
||||
:fade="true"
|
||||
panel-class="max-w-sm"
|
||||
title="新建工单"
|
||||
@update:open="(v: boolean) => { if (!v) closeCreate() }"
|
||||
>
|
||||
<!-- 弹窗头部 -->
|
||||
<div class="flex items-center justify-between px-5 pt-5 pb-2 flex-shrink-0">
|
||||
<h3 class="text-base font-semibold text-gray-900">新建工单</h3>
|
||||
@@ -209,20 +213,6 @@ async function handleDelete(t: TicketListItem, e: Event) {
|
||||
<template v-else>提交工单</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,13 @@ export default defineConfig({
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
// 向下兼容国产浏览器/微信内置内核(日志实测存在 Chrome 78/85/91/97 等机型)。
|
||||
// 默认 target "baseline-widely-available" 会产出可选链/空值合并等新语法,
|
||||
// Chrome 80 以下直接解析失败白屏;统一降到 chrome78(微信 XWEB 常见下限)。
|
||||
// 注:CSS 侧的 @layer/oklch 兼容由 scripts/postbuild-css.mjs 处理。
|
||||
build: {
|
||||
target: 'chrome78',
|
||||
},
|
||||
server: {
|
||||
port: 3300,
|
||||
proxy: {
|
||||
|
||||
Reference in New Issue
Block a user