diff --git a/components.d.ts b/components.d.ts index ad18355..4f85df3 100644 --- a/components.d.ts +++ b/components.d.ts @@ -7,24 +7,36 @@ export {} declare module 'vue' { export interface GlobalComponents { + CardTile: typeof import('./src/components/card/CardTile.vue')['default'] Confetti: typeof import('./src/components/Confetti.vue')['default'] CountUp: typeof import('./src/components/CountUp.vue')['default'] + CouponCard: typeof import('./src/components/coupon/CouponCard.vue')['default'] EmptyState: typeof import('./src/components/EmptyState.vue')['default'] + MineDrawers: typeof import('./src/components/mine/MineDrawers.vue')['default'] PaneLibrary: typeof import('./src/components/home/PaneLibrary.vue')['default'] PaneMine: typeof import('./src/components/home/PaneMine.vue')['default'] + PaneRefresher: typeof import('./src/components/home/PaneRefresher.vue')['default'] PaneWorkbench: typeof import('./src/components/home/PaneWorkbench.vue')['default'] + PromoCodeInput: typeof import('./src/components/member/PromoCodeInput.vue')['default'] QuotaBar: typeof import('./src/components/vip/QuotaBar.vue')['default'] QuotaGate: typeof import('./src/components/vip/QuotaGate.vue')['default'] + SeatMap: typeof import('./src/components/event/SeatMap.vue')['default'] Sticker: typeof import('./src/components/Sticker.vue')['default'] TabBar: typeof import('./src/components/TabBar.vue')['default'] TabIcon: typeof import('./src/components/TabIcon.vue')['default'] + ToolIdle: typeof import('./src/components/tool/ToolIdle.vue')['default'] + ToolPage: typeof import('./src/components/tool/ToolPage.vue')['default'] + ToolRunning: typeof import('./src/components/tool/ToolRunning.vue')['default'] + ToolSliderRow: typeof import('./src/components/tool/ToolSliderRow.vue')['default'] VipCard: typeof import('./src/components/vip/VipCard.vue')['default'] VipDock: typeof import('./src/components/vip/VipDock.vue')['default'] VipFloatAd: typeof import('./src/components/vip/VipFloatAd.vue')['default'] WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default'] + WdDatetimePicker: typeof import('wot-design-uni/components/wd-datetime-picker/wd-datetime-picker.vue')['default'] WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default'] WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default'] WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default'] + WdSwitch: typeof import('wot-design-uni/components/wd-switch/wd-switch.vue')['default'] WdTextarea: typeof import('wot-design-uni/components/wd-textarea/wd-textarea.vue')['default'] } } diff --git a/scripts/selfcheck-naming.mjs b/scripts/selfcheck-naming.mjs new file mode 100644 index 0000000..5ab8181 --- /dev/null +++ b/scripts/selfcheck-naming.mjs @@ -0,0 +1,71 @@ +/** + * A2 自检脚本(可复现):验证「同一秒内两个同名任务 → 两个不等的 savedName」 + * + * 直接加载生产代码 `src/utils/image/name-collision.ts`(零依赖纯函数), + * 由 Node 原生类型剥离执行,无需打包工具: + * + * node scripts/selfcheck-naming.mjs + * + * 结果同时打印到终端并写入 `scripts/selfcheck-naming.log`(UTF-8), + * 避免 Windows 控制台代码页把中文输出弄成乱码。 + * 模拟 `core.ts#saveResult` 的去冲突步骤: + * 候选名 + 「当天历史 ∪ 本次会话已用名」集合 → dedupeName → 命中则追加 -2、-3。 + */ +import { writeFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' +import { dedupeName } from '../src/utils/image/name-collision.ts' + +const lines = [] +let failed = 0 +function assert(cond, msg) { + const line = `${cond ? 'ok ' : 'FAIL'}: ${msg}` + lines.push(line) + if (!cond) + failed++ +} + +// —— 核心场景:同一秒、同工具、同原图,连续产出两次 —— +const candidate = '去除背景-IMG_2043-20260918153012.png' +const taken = [] + +const n1 = dedupeName(candidate, taken) +taken.push(n1) +const n2 = dedupeName(candidate, taken) +taken.push(n2) + +assert(n1 === candidate, `第一次产出保持干净格式:${n1}`) +assert(n2 !== n1, `两次产出文件名不相等:${n1} vs ${n2}`) +assert(n2 === '去除背景-IMG_2043-20260918153012-2.png', `第二次追加 -2:${n2}`) + +// —— 边界:时间戳(14 位数字)绝不能被误判成 `-N` 计数器 —— +assert(n2.endsWith('-20260918153012-2.png'), '时间戳未被当作计数器截断') + +// —— 边界:第三次继续递增 —— +const n3 = dedupeName(candidate, taken) +assert(n3 === '去除背景-IMG_2043-20260918153012-3.png', `第三次追加 -3:${n3}`) + +// —— 边界:主干已带 -N 后缀时从 N+1 起,避免出现 xxx-2-2 —— +assert(dedupeName('x-2.png', ['x-2.png']) === 'x-3.png', '已带后缀时递增不叠加(-2 → -3)') + +// —— 边界:无冲突时原样返回(正常路径不改名) —— +assert(dedupeName('a.png', []) === 'a.png', '无冲突原样返回') + +// —— 边界:无扩展名的候选 —— +assert(dedupeName('plain', ['plain']) === 'plain-2', '无扩展名时追加 -2') + +lines.push('') +lines.push(failed ? `${failed} check(s) FAILED` : 'ALL CHECKS PASSED') + +const out = lines.join('\n') +// eslint-disable-next-line no-console +console.log(out) +try { + const here = dirname(fileURLToPath(import.meta.url)) + writeFileSync(join(here, 'selfcheck-naming.log'), `${out}\n`, 'utf8') +} +catch { + /* 忽略:写日志失败不影响自检结论 */ +} + +process.exit(failed ? 1 : 0) diff --git a/src/App.vue b/src/App.vue index c79c8fb..e8c1cd9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -20,6 +20,12 @@ page { --tb-text: #3d2a1e; --tb-text-2: #9a7b65; --tb-border: #ffe3d3; + /* 卡片质感 token(R2-1):描边高光 + 内高光 + 三档阴影分层。色系不新增 */ + --tb-card-line: rgba(255, 195, 155, 0.55); + --tb-card-hi: rgba(255, 255, 255, 0.92); + --tb-shadow-1: 0 2rpx 6rpx rgba(107, 47, 15, 0.05), 0 10rpx 24rpx rgba(107, 47, 15, 0.08); + --tb-shadow-2: 0 3rpx 10rpx rgba(107, 47, 15, 0.05), 0 16rpx 38rpx rgba(107, 47, 15, 0.1); + --tb-shadow-3: 0 4rpx 12rpx rgba(107, 47, 15, 0.06), 0 22rpx 52rpx rgba(107, 47, 15, 0.13); /* 撞色点缀 */ --tb-mint: #3ecfb2; --tb-lemon: #ffc53d; @@ -68,6 +74,30 @@ page { opacity: 0.92; } +/* 卡片点击热区(P3):承接 tap 的原生 view 包裹层。 + 撑满网格单元高度,让同排 CardTile 视觉等高;子卡 flex 撑满。 + 注:子卡 .ct 自带 overflow:hidden(自动最小尺寸已为 0),故 min-width:0 只需在包裹层声明一次。 */ +.cttap { + display: flex; + min-width: 0; + /* 显式撑满:grid/flex 单元格里不允许出现"卡片比格子窄"的留白(P5 用户反馈②) */ + width: 100%; + align-items: stretch; +} +/* 缺陷 A 修复:MP 下自定义组件宿主(如 )会在 `.cttap` 与 `.ct` 之间包一层, + 使 `.cttap > .ct` 这个**子选择器永不匹配**(H5 单根组件就地渲染、无宿主包裹 → 只在 H5 生效, + H5 会对本 bug 假性通过)。宿主作为 flex 项若不显式撑满,主尺寸退化为 max-content + → 卡片宽度塌成「最长 nowrap 行」内容宽(≈4 个中文字)。 + ⚠️ MP 端宿主撑满**不能**在这里用 `.cttap > *` 兜底 —— WXSS 不支持通配符 `*`, + 真机开发者工具直接报「app.wxss(x:x): error at token '*'」,整个 app.wxss 编译失败 + (uni build EXIT=0 ≠ 微信工具链可编译)。MP 宿主撑满改由 CardTile.wxss 内的 + `:host` 规则承担(见 CardTile.vue + + diff --git a/src/components/coupon/CouponCard.vue b/src/components/coupon/CouponCard.vue new file mode 100644 index 0000000..44ceb92 --- /dev/null +++ b/src/components/coupon/CouponCard.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/src/components/event/SeatMap.vue b/src/components/event/SeatMap.vue new file mode 100644 index 0000000..6a71417 --- /dev/null +++ b/src/components/event/SeatMap.vue @@ -0,0 +1,1004 @@ + + + + + diff --git a/src/components/home/PaneLibrary.vue b/src/components/home/PaneLibrary.vue index e0367f3..9bde218 100644 --- a/src/components/home/PaneLibrary.vue +++ b/src/components/home/PaneLibrary.vue @@ -1,44 +1,143 @@ - + - + diff --git a/src/components/mine/MineDrawers.vue b/src/components/mine/MineDrawers.vue new file mode 100644 index 0000000..0ba7ffa --- /dev/null +++ b/src/components/mine/MineDrawers.vue @@ -0,0 +1,948 @@ + + + + + diff --git a/src/components/tool/ToolIdle.vue b/src/components/tool/ToolIdle.vue new file mode 100644 index 0000000..a93b8db --- /dev/null +++ b/src/components/tool/ToolIdle.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/src/components/tool/ToolPage.vue b/src/components/tool/ToolPage.vue new file mode 100644 index 0000000..60f3e3d --- /dev/null +++ b/src/components/tool/ToolPage.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/src/components/tool/ToolRunning.vue b/src/components/tool/ToolRunning.vue new file mode 100644 index 0000000..eca6a66 --- /dev/null +++ b/src/components/tool/ToolRunning.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/src/components/tool/ToolSliderRow.vue b/src/components/tool/ToolSliderRow.vue new file mode 100644 index 0000000..fa84eff --- /dev/null +++ b/src/components/tool/ToolSliderRow.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/src/components/vip/QuotaBar.vue b/src/components/vip/QuotaBar.vue index 5686508..bf6a054 100644 --- a/src/components/vip/QuotaBar.vue +++ b/src/components/vip/QuotaBar.vue @@ -13,6 +13,7 @@ import type { ToolQuota } from '@/api/quota' import { computed, onMounted, ref, watch } from 'vue' import { useQuotaStore } from '@/stores/quota' import { currentToolKey, fetchToolQuota, goBuyPack, goMemberCenter, lastConsumedQuota } from '@/utils/quota' +import { Z } from '@/utils/z-index' const props = withDefaults(defineProps<{ toolKey?: string @@ -84,7 +85,7 @@ function buyMember() { @@ -142,7 +143,6 @@ function buyMember() { diff --git a/src/pages/mine/profile.vue b/src/pages/mine/profile.vue index 7e17ac5..1c3357e 100644 --- a/src/pages/mine/profile.vue +++ b/src/pages/mine/profile.vue @@ -5,6 +5,7 @@ import Sticker from '@/components/Sticker.vue' import { useUserStore } from '@/stores/user' import { AVATAR_PRESETS, presetOf } from '@/utils/avatar' import { navPadTop } from '@/utils/safearea' +import { Z } from '@/utils/z-index' const userStore = useUserStore() /** 导航栏顶部留白:MP 用真实状态栏高度 */ @@ -13,6 +14,8 @@ const navPad = navPadTop() const nickname = ref('') const avatarUrl = ref('') const saving = ref(false) +/** 微信头像上传中 */ +const uploading = ref(false) function syncFromStore() { const u = userStore.user @@ -31,6 +34,34 @@ function pickAvatar(key: string) { avatarUrl.value = key } +/** + * 微信头像(MP): + + + {{ '默认头像' }} + - + diff --git a/src/pages/tools/image/annotate.vue b/src/pages/tools/image/annotate.vue index 162edff..edeb32b 100644 --- a/src/pages/tools/image/annotate.vue +++ b/src/pages/tools/image/annotate.vue @@ -76,6 +76,14 @@ async function initCanvas(cssW: number, cssH: number) { el.height = ch * dpr canvasNode = el ctx = el.getContext('2d') + // ⚠️ H5 专属:uni-h5 运行时 `initHidpi()` 会 monkey-patch `CanvasRenderingContext2D.prototype`, + // 对所有 `__hidpi__ === true` 的 context 把绘制坐标(fillRect/drawImage/fillText/arc/...)自动 + // ×devicePixelRatio;uni 的 canvas 组件(hidpi 默认 true)会给本内层 canvas 的 ctx 打上该标记。 + // 本页的 逻辑坐标→高分辨率位图 映射已由下面的 `scale(dpr, dpr)` 完成,若再叠加 uni 的 ×dpr 会变成 + // **dpr²**:DPR≥2 时编辑画布与「保存到相册」的导出图都会被裁(只剩左上角放大,DPR=3 时整幅全红)。 + // 故此处显式关掉 uni 的这层自动缩放;用条件编译隔离,**MP 端不受影响**(MP 无此补丁)。 + if (ctx) + (ctx as any).__hidpi__ = false ctx?.scale(dpr, dpr) } resolve() diff --git a/src/pages/tools/image/avatarfx.vue b/src/pages/tools/image/avatarfx.vue index 547d76e..48997b3 100644 --- a/src/pages/tools/image/avatarfx.vue +++ b/src/pages/tools/image/avatarfx.vue @@ -3,6 +3,7 @@ import type { FxShape } from '@/utils/image/avatarfx' import { onReady } from '@dcloudio/uni-app' import { computed, ref } from 'vue' import Sticker from '@/components/Sticker.vue' +import ToolSliderRow from '@/components/tool/ToolSliderRow.vue' import VipDock from '@/components/vip/VipDock.vue' import { AVATAR_EFFECTS, drawEffect, drawPhoto } from '@/utils/image/avatarfx' import { beginDraw, bindImageCanvas, chooseOneImage, exportFile, loadImage, saveResult } from '@/utils/image/core' @@ -182,35 +183,29 @@ function goBack() { - - 照片缩放 - - - - 描边粗细 - - diff --git a/src/pages/tools/image/collage.vue b/src/pages/tools/image/collage.vue index b89fdb4..3ad887e 100644 --- a/src/pages/tools/image/collage.vue +++ b/src/pages/tools/image/collage.vue @@ -1,6 +1,7 @@ diff --git a/src/pages/tools/image/qrcode.vue b/src/pages/tools/image/qrcode.vue index fe39f9e..92831ac 100644 --- a/src/pages/tools/image/qrcode.vue +++ b/src/pages/tools/image/qrcode.vue @@ -1,20 +1,68 @@