Compare commits

...

47 Commits

Author SHA1 Message Date
3b81ecdf98 Merge branch 'dev/1.0' of http://git.aigc-quickapp.com/Uniapp/lucky_shop into temp2797 2026-08-13 14:11:20 +08:00
c536fac4d2 chore:视频显示布局正常 2026-08-13 11:15:57 +08:00
475449f04e chore:视频正常上下黑边删除 2026-08-13 10:32:35 +08:00
e40b46a888 chore:视频可以播放 2026-08-13 10:20:27 +08:00
a81386d336 refactor(components): 统一多端textarea配置与高度计算逻辑
1.  对齐ns-form和ns-newform的textarea配置参数,新增textareaRows和textareaLineHeight配置
2.  移除平台分支判断,所有平台统一使用:rows+行高计算高度的方案
3.  适配鸿蒙ArkUI原生TextArea,通过:rows控制行数,JS动态计算高度
4.  移除textarea硬编码高度,改为通过formConfig动态控制
2026-08-12 15:47:11 +08:00
b69eeba74b fix(ns-form): 适配微信小程序和H5的textarea渲染逻辑
重构了多平台textarea的渲染条件,合并了平台判断逻辑,新增rows属性配置,优化了样式适配和代码可读性
2026-08-12 15:41:06 +08:00
82139e616d chore:后台国际化可以控制查看详情按钮 2026-08-10 14:18:24 +08:00
ed6d9f563d chore:后台设置二级分类,模板1显示“查看详情”按钮 2026-08-10 13:59:44 +08:00
3a8ad7d871 refactor(diy-recommend-code): 动态 style 全改用 $util.objToStyle 输出 px
问题:
- 后端 diyData 数值语义是 px(后端 wap/design.html 都按 px 输出),
  但前端 px2rpx() 之前 *2 转 rpx,导致商家设置 8px 实际渲染 16px
- 鸿蒙元服务对动态 style 中的 rpx 解析有问题

方案:
- px2rpx(val) 改为返回原值(当作 px 用)
- 17 处动态 :style="{...}" 全部改成 :style="$util.objToStyle({...})"
  用 objToStyle 工具方法生成标准 CSS 字符串,跨平台兼容
- 34 处 px2rpx() + 'rpx' 拼接改成 + 'px'
- 8 处 padding/border 中的 'rpx ' / 'rpx solid' 也同步改成 'px'

静态 'XXrpx' 字符串(写在模板里的)保留,uniapp 静态模板
支持 rpx,不影响。

后端保持原状不动(数值 px 语义,与 wap/design.html 一致)。
这样前后端语义统一,商家在 diyData 改数值时:
- H5/小程序: 静态 rpx 自适应屏幕
- 动态部分: 按 px 渲染,商家设计器里的 px 值精确生效
- 鸿蒙元服务: 全部用 px,无解析问题
2026-08-08 15:43:13 +08:00
ec960a29c4 fix(diy-recommend-code): 复制按钮 style 单位 rpx 改 px 适配鸿蒙元服务
之前复制按钮用 rpx 单位:
- border:2rpx solid #xxx
- font-size:24rpx
- height:60rpx / padding:0 32rpx / border-radius:30rpx / gap:8rpx

在 H5/微信小程序没问题,但鸿蒙元服务对动态拼接 style 中的 rpx
解析异常,渲染尺寸可能错乱。

修复: rpx 全改成 px(1rpx = 0.5px,即原数值 / 2):
- border:1px solid #xxx     (原 2rpx)
- font-size:12px            (原 24rpx)
- border-radius:15px        (原 30rpx)
- padding:0 16px            (原 0 32rpx)
- height:30px               (原 60rpx)
- gap:4px                   (原 8rpx)

horizontal 预设:
- border-radius:16px        (原 32rpx)
- padding:0 12px            (原 0 24rpx)
- height:32px               (原 64rpx)
- gap:3px                   (原 6rpx)

这样 4 处复制按钮 + 文字节点所有动态 style 数值都用 px,
鸿蒙元服务全平台兼容。
2026-08-08 15:01:52 +08:00
8312de5d09 refactor(diy-recommend-code): 复制按钮用 $util.objToStyle 生成 style
之前复制按钮用字符串拼接方式:
  :style="'...static...' + method() + '...static...'"

虽然字符串拼接全平台兼容,但写法繁琐,且 camelCase/kebab-case
容易写错(camelCase 用驼峰,字符串里要手写 - 转换)。

改成调用 common/js/util.js 中已有的 objToStyle 方法:
  :style="$util.objToStyle(copyBtnStyleObj(...))"

copyBtnStyleObj(preset, textOnly) 返回对象:
- 边框/背景/border-color: 跟随后端 diyData.codeColor
- 文字色/字号/字重: 跟随后端 codeColor(可选 textOnly=true 时)
- 其余布局属性: 由 preset 参数覆盖(4 个预设各自差异)

$util.objToStyle 自动 camelCase → kebab-case(如 alignItems → align-items)
转换后拼接为标准 CSS 字符串,全平台兼容。

好处:
- 写法更清晰(对象语法)
- 不用手写 kebab-case
- 与项目其他地方统一风格(common/js/util.js 已有此工具)
2026-08-08 14:57:39 +08:00
5833a244de refactor(diy-recommend-code): 复制按钮 style 改用字符串拼接兼容多端
之前 4 个预设(gradient/minimal/card/horizontal)的"复制推荐码"按钮
用 :style="{...}" 对象语法 + value.codeColor || '#xxx' 三元拼接。

对象语法在 H5 / 小程序正常,但在鸿蒙元服务等部分平台对嵌套对象/
三元拼接的支持有限,容易渲染异常。

修改:
- 新增 methods:
  copyBtnBorderStyle()  → 返回 '2rpx solid #xxx'
  copyBtnTextColor()    → 返回 '#xxx'
- 4 处复制按钮改成 :style="'...字符串...' + 方法() + '...字符串...'"
  风格,只把边框/文字色动态部分用方法返回,
  其余布局属性(display/border-radius/padding/gap 等)写成静态字符串。
- 字符串拼接方式在 H5 / 小程序 / APP / 鸿蒙元服务全平台表现一致

copyBtnBorderStyle() 与 copyBtnTextColor() 都从 diyData.codeColor 读取,
商家改 codeColor 时所有预设的复制按钮边框/文字色都跟随变化。
2026-08-08 14:51:57 +08:00
92fe95d131 feat(diy-recommend-code): 复制按钮颜色跟随后端 diyData.codeColor
之前 gradient 和 horizontal 预设的"复制推荐码"按钮边框/文字颜色
是写死的:
- gradient: #DC143C
- horizontal: #FF8F00

minimal 和 card 已经跟随后端 codeColor 字段。统一所有预设都跟随
codeColor,让商家可以在后端 diyData 调整颜色。

修复:
- gradient 复制按钮: 边框/文字改用 '2rpx solid ' + (value.codeColor || '#DC143C')
  文字色改用 value.codeColor || '#DC143C'
- horizontal 复制按钮: 边框/文字改用 '2rpx solid ' + (value.codeColor || '#FF8F00')
  文字色改用 value.codeColor || '#FF8F00'

这样 4 个预设风格统一,商家改 codeColor 时所有预设的复制按钮边框/
文字色都会跟随。
2026-08-08 14:47:52 +08:00
07718235c3 Revert "fix(diy-recommend-code): horizontal 预设"立即分享"颜色对齐后端 wap.html"
This reverts commit 183181a15d.
2026-08-08 14:42:04 +08:00
183181a15d fix(diy-recommend-code): horizontal 预设"立即分享"颜色对齐后端 wap.html
对比后端 PHP wap.html(第 174 行):
  background:linear-gradient(90deg,#FF9800 0%,#FF4444 100%);
  box-shadow:0 2px 6px rgba(255,68,68,0.25);

前端 horizontal 预设的"立即分享"按钮之前用黄橙渐变
  linear-gradient(90deg,#FFC107 0%,#FF8F00 100%)

不一致。改为后端的橙红渐变:
  linear-gradient(90deg,#FF9800 0%,#FF4444 100%);
  box-shadow 也改为 rgba(255,68,68,0.25)

影响两个分支:
- 小程序 button 路径(button path 行 298)
- H5 view 路径(view path 行 303)

其他 3 个预设(gradient/minimal/card)颜色与后端已对齐,无需调整。
2026-08-08 14:38:29 +08:00
a4f2c81528 fix(diy-recommend-code): 海报背景 + 分享链接绘制
问题 1: 海报整张画成蓝色渐变
原因: _drawPosterBg 用 diyData.bgColorStart/End 当整个画布背景,
但这两个字段实际是给"卡片"用的渐变色,不是给整张海报用的。

修复: _drawPosterBg 只用 bgColor(浅色)填充整张画布;
bgColorStart/End 留给卡片本身(推荐码区)的样式。

问题 2: 后端 showLink=true 时没有绘制分享链接

修复: 在标题副标题后,推荐码区前加一行分享链接
(读 v.shareLink || this.shareLink,截断 36 字符)。
showLink=false 时不画,blockY 保持 88;
showLink=true 时画一行,blockY 下移到 104,推荐码区跟着下移
避免重叠。

drawQrcodeAndExport 的 qrY = blockY + blockH + 24 自动跟随,
底部提示也跟随,整张布局保持完整。
2026-08-08 14:05:11 +08:00
30b66ad7b3 feat(diy-recommend-code): 海报颜色布局改为读取 diyData 动态适配
之前 generateSharePoster 颜色和布局都是 hardcode(米黄区、
黄橙渐变、FFC107 等)。后端改 stylePreset 后分享图不会跟随变化。

修改: 抽出 palette 对象,所有视觉参数都从 diyData.value 读:
- bgColor / bgColorStart / bgColorEnd / borderColor
- borderRadius / padding
- titleColor / titleFontSize
- codeColor / codeFontSize
- accentColor / textColor

新增方法:
- _drawPosterBg(ctx, W, H, palette)
  优先用 createLinearGradient 画 bgColorStart→bgColorEnd 渐变,
  旧 canvas 不支持则 fallback 到 bgColor 单色
- _drawPosterCard(ctx, W, H, palette, stylePreset)
  按 stylePreset 区分装饰:minimal/card/horizontal 右上角加 🎁 emoji
  gradient 不加装饰

后续如需更多差异,只在这两个方法里扩展即可,不影响主流程。
2026-08-08 13:58:55 +08:00
077fac40c2 fix(diy-recommend-code): 兼容 setFontWeight 旧 canvas API
用户日志: TypeError: c.setFontWeight is not a function
原因: setFontWeight 是微信小程序新版 type=2d canvas 专有 API,
旧版不支持。运行时 ctx.setFontWeight 不存在,调用报错。

修复: 先用 typeof 检查,存在则调用;不存在则用
ctx.font = 'bold 36px sans-serif' 字符串设置字重(旧 API 支持)。
2026-08-08 12:03:46 +08:00
10d45b1c3b fix(diy-recommend-code): generateSharePoster 等方法移到 methods 块
问题: 用户日志显示 "TypeError: this.generateSharePoster is not a function"。
原因: generateSharePoster / _exportCanvas / _roundRect / _downloadImage
等方法之前被定义在 methods 块外的 export default 对象上(与
onShareAppMessage 这种生命周期钩子同级),Vue 不会自动绑定它们到
组件实例,导致 this.xxx 找不到。

修复:
- 删除 methods 块外的那 5 个方法的旧定义
- 重新加到 methods 块内(放在 goLogin 后,methods 块关闭 } 之前)
- onShareAppMessage / onShareTimeline 是小程序生命周期钩子,
  必须留在 export default 对象上,不能进 methods(保留不动)

验证:
- H5 测试 isH5Platform=true,提前 return 不进入 generateSharePoster,正常
- 小程序下 this.generateSharePoster 现在能找到方法
- loadCode 成功后 _prepareSharePoster 调用正常
2026-08-08 12:02:11 +08:00
60dc054383 fix(diy-recommend-code): canvas 海报生成加详细调试日志 + ctx.draw 包裹
问题: 小程序下 generateSharePoster "开始调" 后无 "resolved",
卡在 canvas 绘制或导出步骤。

修复 + 调试:

1. generateSharePoster 流程加详细日志:
   - 进入
   - 准备 createCanvasContext
   - createCanvasContext 返回值
   - qrcodeRenderUrl 当前值
   - drawQrcodeAndExport 进入
   - ctx.draw(false, cb) 准备
   - ctx.draw callback 触发

2. 关键修复: ctx.draw(false, callback) 包裹导出
   - 旧代码:画完 fill() 后直接调 _exportCanvas(canvas 还是空的)
   - 新代码:画完后 ctx.draw(false, () => _exportCanvas())
     在 callback 里导出真正渲染完的 canvas

3. _exportCanvas 简化:
   - 不再走 createSelectorQuery(可能挂起)
   - 直接用 canvasId 调 canvasToTempFilePath
   - 加 5s 超时兜底,避免永远卡住

4. _prepareSharePoster 加 isH5Platform 短路:
   - H5 下 canvas 模板不渲染,createCanvasContext 会失败
   - 短路后不调用 generateSharePoster,避免无效报错

请在微信开发者工具里再次测试,看哪一行日志卡住
(预期: 进入 → createCanvasContext 返回 → qrcodeRenderUrl →
drawQrcodeAndExport → ctx.draw 准备 → ctx.draw callback → _exportCanvas → 成功)
,把卡住那一行的日志贴出来。
2026-08-08 11:52:57 +08:00
ef8c7568ac chore(diy-recommend-code): 海报预生成流程加调试日志
增加 _prepareSharePoster 流程日志:
- _prepareSharePoster 被调用 + isH5Platform/code
- 开始调 generateSharePoster
- generateSharePoster resolved + posterPath
- 写 storage 完成

方便在微信开发者工具/真机控制台调试为什么没看到
[推荐码分享]海报预生成完成 日志,定位是 _prepareSharePoster
没被调用、generateSharePoster 抛错、还是写 storage 失败。
2026-08-08 11:48:55 +08:00
551d6c103d fix(diy-recommend-code): 修复小程序分享图仍为全屏截图
之前的方案是用户点"立即分享"按钮时,onShareClick 异步调
generateSharePoster() 生成海报后写 storage。但这有问题:

微信小程序 <button open-type="share"> 流程:
1. 用户点 button → 同步触发页面级 onShareAppMessage
2. onShareAppMessage 同步返回 shareData(必须含 imageUrl)
3. 微信用这个 imageUrl 展示分享面板

onShareClick 的异步 generateSharePoster 永远来不及,onShareAppMessage
读取 storage 时海报还没生成,所以微信用全屏截图。

修复:预生成海报。
- 在 loadCode 成功回调里,推荐码拿到后立即调 _prepareSharePoster()
  异步生成海报,生成完写入 _recommendShareData storage
- loadQrcodeIfNeeded 成功回调里也再调一次,确保带二维码海报覆盖
  不带二维码的占位版

_prepareSharePoster():
- #ifdef MP-WEIXIN 包住,仅小程序执行
- 调 generateSharePoster 拿 tempFilePath
- 写入 _recommendSharePoster 缓存 + _recommendShareData 当前值
- 用户点分享时,storage 已经有最新海报,onShareAppMessage
  读取后返回 imageUrl,微信用这个图展示分享面板
2026-08-08 11:46:23 +08:00
4a92d9bec2 feat(diy-recommend-code): 微信小程序分享海报自动生成(仅推荐码组件)
之前微信小程序点击"立即分享"会触发 onShareAppMessage,
默认 imageUrl 用 diyData.shareImageUrl(为空时微信截全页)。
问题是分享图片是整张页面,而不是推荐码组件本身。

新增 generateSharePoster():用 uni.canvas 绘制一张仅含推荐码组件内容
的 375x480 海报:
- 白底 + 顶部标题
- 米黄背景区 + 推荐码大字
- 二维码(如有 qrcodeRenderUrl)
- 底部提示文字

并修改 onShareAppMessage:
- 小程序环境(!isH5Platform):返回 Promise,等待 canvas 生成完成后
  用临时文件路径作 imageUrl
- 失败回退到 diyData.shareImageUrl
- H5 环境:保持原逻辑(不走 canvas)

新增辅助方法:
- _exportCanvas():canvas 转临时文件路径(支持 type=2d 和旧版 API)
- _roundRect():绘制圆角矩形路径(canvas 2d 不直接支持 roundRect 时的兜底)
- _downloadImage():网络图片下载到临时路径(uni.downloadFile Promise 化)

模板:
- 在 root view 内加一个隐藏 canvas (canvas-id="sharePosterCanvas"),
  仅小程序环境渲染,固定在视口外 left:-9999px top:-9999px
2026-08-08 11:39:25 +08:00
ff4340c3a9 style(diy-recommend-code): horizontal 预设按钮行两端对称对齐
horizontal 预设的"复制推荐码"和"立即分享"按钮之前是错落布局
(align-items:flex-end),且两端外边距不一致(padding 不统一)。
改为两端对称对齐:

- 父级 view:
  align-items:center (垂直居中,不再错落)
  padding:0 8rpx (两端外边距对称)
- 复制按钮:
  display:inline-flex (按内容自适应宽度)
  border:2rpx solid #FF8F00 (固定橙色边框,不读 diyData.codeColor)
- 立即分享按钮 (button path):
  position:relative; left:0; right:0; margin:0; padding:0
  (修复小程序 button 默认偏移问题,与其他预设 button path 一致)

效果:
- H5: 复制按钮左边缘距卡片左边 = 分享按钮右边缘距卡片右边 (各 8rpx)
- 小程序: 立即分享按钮不再向左偏移,与复制按钮保持视觉对称
- 两个按钮高度统一,垂直居中对齐
2026-08-08 11:29:04 +08:00
4303a93278 style(diy-recommend-code): card 预设同步采用 100% 宽 + 居中结构
card 预设的"立即分享"按钮之前是 inline-block + padding 自适应宽度,
与其他预设不一致。改为和 gradient / minimal 预设一致的"双层结构":

- button path (小程序/APP):
  外层 button: position:relative; left:0; right:0;
  margin:16rpx 0 0 0; width:100%; display:block; box-sizing:border-box;
  内层 view: display:block; width:100%; height:76rpx;
  background:#FFFFFF; border:2rpx solid #FF6B6B; border-radius:38rpx;
  text-align:center;

- view path (H5):
  外层 view: margin-top:16rpx; width:100%;
  内层 view: display:block; width:100%; height:76rpx;
  同上样式

card 风格保持"白底红边框"设计,不读 diyData.buttonBgColor。

效果:
- 按钮 100% 卡片宽,与 gradient / minimal 一致
- 小程序 button 默认偏移问题(position:relative + left:0 + right:0)同步修复
- 商家在 4 个预设下看到一致的全宽分享按钮布局
2026-08-08 10:35:12 +08:00
12e4493a17 style(diy-recommend-code): minimal 预设同步采用 100% 宽 + 居中结构
minimal 预设的"立即分享"按钮之前是 inline-block + padding 自适应宽度,
与其他预设不一致。改为和 gradient 预设一致的"双层结构":

- button path (小程序/APP):
  外层 button: position:relative; left:0; right:0;
  margin:16rpx 0 0 0; width:100%; display:block; box-sizing:border-box;
  内层 view: display:block; width:100%; height:76rpx;
  background:linear-gradient(90deg,#FF6B6B 0%,#FF4444 100%);
  border-radius:38rpx; box-shadow; text-align:center;

- view path (H5):
  外层 view: margin-top:16rpx; width:100%;
  内层 view: display:block; width:100%; height:76rpx;
  同上渐变样式

效果:
- 按钮 100% 卡片宽,与 gradient 一致
- 小程序 button 默认偏移问题(position:relative + left:0 + right:0)同步修复
- 商家在 4 个预设下看到一致的全宽分享按钮布局
2026-08-08 10:29:31 +08:00
c94e214d05 style(diy-recommend-code): 优化分享按钮布局样式
调整分享按钮的内外层元素排版,统一宽高对齐方式,修复按钮展示偏移问题,提升多端视觉一致性
2026-08-08 10:11:14 +08:00
afede79047 fix(diy-recommend-code): 小程序 button 路径同步采用双层居中结构
之前 H5 路径(v-else-if 走 view)已修复为双层结构
(外层 text-align:center + 内层 inline-block),但小程序/APP
路径仍保留旧的「display:flex + width:100% + height:80rpx +
box-shadow」组合,在 webview 中可能存在同样问题。

为了让小程序下居中也稳定,4 个预设的 <button open-type="share">
也改为双层结构:
- 外层 button: text-align:center;background:transparent;
  border:none;padding:0;line-height:normal;
  保留 open-type="share" 让微信小程序原生分享能力依然有效
- 内层 view: display:inline-block; 自带 padding+渐变+阴影样式

horizontal 预设的 button (行 298) 是水平按钮行右侧窄按钮,
额外加 display:inline-block 让其按内容撑开(不被推到 100% 宽)。

这样:
- H5: view fallback(text-align:center)→ 居中
- 小程序: button + 内联 view(双层结构)→ 居中
- APP: 与小程序走同一份代码 → 居中

注意:本次修改的 HTML 结构对小程序的 onShareAppMessage 钩子
无影响(uniapp 仍按 v-if 条件编译 + open-type="share" 自动桥接)。
2026-08-08 09:36:57 +08:00
ab727eef1d fix(diy-recommend-code): H5 立即分享按钮居中问题修复
之前 H5 平台下 view fallback 的样式:
  display:flex; justify-content:center; align-items:center;
  width:100%; height:80rpx; ...
在某些情况下未生效(可能与 box-shadow / border-radius / box-sizing
互动有关),导致按钮在 H5 中贴左显示。

修复方案:改用经典的双层结构:
- 外层 view: text-align:center; (行内元素自动居中)
- 内层 view: display:inline-block; width 按 padding 自适应
这种结构 uniapp H5 / 小程序 / APP 各端都稳定支持。

修改点:
- gradient 预设(L119): 黄橙渐变胶囊
- minimal 预设(L187): 红橙渐变胶囊
- card 预设(L238): 白底红边胶囊
- horizontal 预设(L295): 不变(靠右浮动按钮,已有 flex-end 对齐)
- gradient 复制按钮父级(L106): 同步改 text-align:center + inline-flex
  (修复 H5 下 fit-content + flex 在某些机型失效导致按钮偏左)

影响:仅修改 H5 平台的 view fallback 样式(实际渲染层),小程序/APP
端 button 路径未改动。
2026-08-08 09:19:23 +08:00
ee050f5df9 chore(.gitignore): 归档本地测试文件到 _test_archive/
将之前调试时遗留的 43 个测试脚本/截图/对比图/草稿文件
移动到 _test_archive/ 子目录,并加 .gitignore 规则使其不入仓。

规则:
- _test_archive/ : 整个归档目录
- _test_*.png / _test_*.html : 根目录兜底
- _compare*.png / _compare*.py
- _auth_*.json
- _commit_msg.txt
- /check_button.py

_test_v6_clean.py 同时修改,所有截图路径改为 OUT_DIR = __file__ 目录,
保证未来运行测试时产物仍归档到 _test_archive/ 而不是污染根目录。
2026-08-07 18:28:12 +08:00
855fbb2fe5 Merge branch 'dev/1.0' of https://git.aigc-quickapp.com/Uniapp/lucky_shop into dev/1.0 2026-08-07 18:24:13 +08:00
b706e67e82 chore(gitignore): add test archive directory to gitignore
新增本地测试脚本与产物归档的git忽略规则,避免将测试临时文件提交到公共仓库
2026-08-07 18:24:08 +08:00
557addb76b Merge branch 'dev/1.0' of http://git.aigc-quickapp.com/Uniapp/lucky_shop into Branch_24cb3a09 2026-08-07 18:14:15 +08:00
dacc25b281 chore:微信小程序推荐码登录按钮正常弹出手机号授权 2026-08-07 18:13:35 +08:00
ec4fb3e688 feat(diy-recommend-code): 二维码容器支持尺寸/边框/背景/圆角/内边距可配
新增 6 个 props 字段:
- qrSize: 数字,默认 200,二维码图片宽高(rpx)
- qrBoxBorderColor: 颜色,默认 '',容器边框色(空=按预设默认)
- qrBoxBorderWidth: 数字,默认 0,边框宽度(rpx,0=按预设默认)
- qrBoxBorderRadius: 数字,默认 0,容器圆角(rpx)
- qrBoxBackground: 颜色,默认 '',容器背景色(空=透明)
- qrBoxPadding: 数字,默认 0,容器内边距(rpx)

修改点:4 个预设(gradient/minimal/card/horizontal)的 showQR 块
不再用硬编码 <image width=200rpx>,改为:
  <view style="<动态 width/height/border/borderRadius/bg/padding>">
    <image style="width:100%;height:100%">
  </view>
保证缩放、边框、背景、圆角、内边距都能响应 props 字段。

后端同步:design.html / wap.html 新增 nc.qrSize / nc.qrBoxBorderColor /
  nc.qrBoxBorderWidth / nc.qrBoxBorderRadius / nc.qrBoxBackground /
  nc.qrBoxPadding 字段并应用。

注:单位上,前端 rpx;后端 px。两者各自适配,与现有 fontSize/
  borderRadius/titleFontSize 等字段命名一致。
2026-08-07 18:06:25 +08:00
75e40c752b feat(diy-recommend-code): 复制推荐码按钮支持显示/文案自定义
新增 props 字段:
- showCopyButton: 布尔,默认 true,控制"复制推荐码"按钮显示
- copyButtonText: 字符串,默认 "复制推荐码",自定义按钮文案

修改点:
- props default: 新增 showCopyButton: true、copyButtonText: '复制推荐码';
                  移除未使用的 placeholderCopyText、placeholderShareText、
                  showButton,统一用 showShareButton 名称
- 4 个预设(gradient/minimal/card/horizontal)的"复制推荐码"按钮
                  包裹层加 v-if="value.showCopyButton !== false"
                  按钮文案改为 {{ nc.copyButtonText || '复制推荐码' }}

后端同步: design.html / wap.html 同步加 nc.showCopyButton / nc.copyButtonText 字段。
2026-08-07 17:53:26 +08:00
24df6f0dd5 chore(diy-recommend-code): 同步 titleColor 默认值与后端 design.html 对齐
后端 design.html 的 titleColor fallback 是 '#1A1A1A',4 个预设都使用这个值。
前端 props default 之前是 '#333333',导致 DIY 后台预览(design.html)和
H5 实际渲染(前端组件)标题颜色不一致。

修复:前端 titleColor 改为 '#1A1A1A',与后端对齐。

注意:前端 H5 渲染的样式仍然优先读取 props.value.titleColor(来自后端 diyData),
只有当后端没有传 titleColor 时才落到这个默认值,所以对线上渲染无破坏。
2026-08-07 17:40:44 +08:00
a8ab093eae fix(diy-recommend-code): Minimal 风格立即分享按钮改红橙渐变胶囊
之前后端 diyData.buttonBgColor 默认 #E4393C 覆盖了 Minimal 风格的
linear-gradient(90deg,#FF6B6B 0%,#FF4444 100%) 默认值,导致「立即分享」
按钮显示为红色实心,与设计参考图不一致。

调整内容:
- 立即分享按钮固定走红橙渐变 linear-gradient(90deg,#FF6B6B 0%,#FF4444 100%)
  不再读 diyData.buttonBgColor(与 Card/Gradient/Horizontal 风格保持统一)
- H5 端 view fallback 同步(与已有 H5 风格统一处理)

影响范围:仅 Minimal 极简风格(stylePreset === 'minimal')。
2026-08-07 17:28:15 +08:00
7f4b1d6d13 fix(diy-recommend-code): Horizontal 风格立即分享按钮改黄橙渐变胶囊
之前后端 diyData.buttonBgColor 默认 #E4393C 覆盖了 Horizontal 风格的
`linear-gradient(90deg,#FF9800 0%,#FF4444 100%)` 默认值,导致「立即分享」
按钮显示为红色实心,与设计参考图不一致。

调整内容:
- 立即分享按钮固定走 linear-gradient(90deg,#FFC107 0%,#FF8F00 100%)
  不再读 diyData.buttonBgColor(与 Gradient 风格保持统一)
- 阴影色从 rgba(255,68,68,...) 改为 rgba(255,143,0,...) 与渐变色一致
- H5 端 view fallback 同步(与 Card / Gradient 风格统一处理)

影响范围:仅 Horizontal 横向风格(stylePreset === 'horizontal')。
2026-08-07 17:24:31 +08:00
1420b7435e fix(diy-recommend-code): Gradient 风格立即分享按钮改胶囊形 + 黄橙渐变
之前后端 diyData.buttonBgColor 默认 #E4393C 会覆盖 Gradient 风格的
linear-gradient 默认值,导致「立即分享」按钮显示为红色实心。

调整内容:
- 复制按钮顶部距 12rpx → 16rpx(更均匀)
- 立即分享按钮 marginTop 20rpx → 16rpx(紧凑布局)
- 立即分享按钮圆角 16rpx → 40rpx(胶囊形)
- 立即分享按钮固定走 linear-gradient(90deg,#FFC107 0%,#FF8F00 100%),
  不再读 diyData.buttonBgColor(避免被后端默认色覆盖)
- H5 端 view fallback 同步(与 Card 风格统一处理)

影响范围:仅 Gradient 渐变风格(stylePreset === 'gradient')。
2026-08-07 17:21:12 +08:00
8b331e53d3 fix(diy-recommend-code): Card 风格立即分享按钮固定「白底红边框」
之前 Button 背景色由 diyData.buttonBgColor 决定,后端默认 #E4393C 导致
Card 风格的「立即分享」按钮显示为红色实心,与设计参考图不一致。

调整内容:
- props default 中 buttonBgColor 默认值改为空字符串
- 新增 isH5Platform 状态(条件编译 H5 段在 created 中标记)
- H5 端用 view 替代 button 触发分享,避开浏览器 button 默认样式
- 小程序/APP 端保留 button 以触发 open-type="share"
- Card 预设的「立即分享」按钮固定为白底 + 红边 + 红字设计

影响范围:仅 Card 卡片风格(stylePreset === 'card')。
2026-08-07 17:16:52 +08:00
10fd785b4c refactor(formdata): 适配H5端保留原生导航栏
调整自绘导航栏逻辑,仅在非H5端渲染自定义导航栏,补充H5端相关注释说明,修正样式代码位置
2026-08-07 16:45:14 +08:00
2c0b8ed076 docs(formdata): add notes about show_home btn effective scope
补充说明show_home参数仅在custom导航栏端生效,H5端忽略该参数
2026-08-07 16:25:19 +08:00
ab8625ebaf feat(form): add custom navigation bar for form page across multiple platforms
适配APP、微信小程序、华为快应用和鸿蒙元服务,新增自定义导航栏,支持返回和首页按钮,修正多端导航栏适配逻辑
2026-08-07 16:22:50 +08:00
bbef0ecf55 feat(util): add coerceBool function for parsing URL query booleans
添加专门用于处理URL查询参数的布尔值转换函数,解决直接使用Boolean转换字符串'false'、'0'时不符合预期的问题,兼容多种合法的布尔值参数写法。
2026-08-07 16:09:02 +08:00
671c350d95 feat(diy-recommend-code): 为推荐码组件添加可定制化样式配置
1.  支持自定义标题字体大小、颜色,推荐码字体大小和颜色
2.  支持自定义卡片圆角、内边距
3.  支持自定义分享按钮背景色、文字颜色
4.  新增分享按钮显示控制、占位推荐码和换码文案配置
5.  统一将硬编码样式替换为动态绑定的可配置样式
2026-08-07 15:00:52 +08:00
2d19a653c9 refactor(form): remove unused back home button and related code
移除了表单页面中不再使用的返回主页按钮、对应的国际化文案和重置表单方法,清理冗余代码
2026-08-07 14:36:28 +08:00
16 changed files with 2372 additions and 1682 deletions

15
.gitignore vendored
View File

@@ -61,6 +61,21 @@ stub-app-projects/mp-harmony/hvigor
# Trae/IDE 临时文件 # Trae/IDE 临时文件
.hvigor/outputs .hvigor/outputs
.trae_permission_check_* .trae_permission_check_*
# 本地测试脚本与产物归档(不入 git, 留本地复用)
# 内容: Playwright 截图 / 测试脚本 / 对比图 / 临时提交草稿
# 复用方式: cd _test_archive && python xxx.py
_test_archive/
# 兜底:防止任何临时测试文件被遗漏在根目录
# 名称以 _test_/_compare_/_auth_ 开头, 或 check_button.py 等
_test_*.png
_test_*.html
_compare*.png
_compare*.py
_auth_*.json
_commit_msg.txt
/check_button.py
# 鸿蒙签名材料 (敏感, 绝不能提交到公共 git) # 鸿蒙签名材料 (敏感, 绝不能提交到公共 git)
# 仓库有 origin (github.com/Zajd/jsysq-uniapp) 是公开仓库, 签名材料必须 gitignore # 仓库有 origin (github.com/Zajd/jsysq-uniapp) 是公开仓库, 签名材料必须 gitignore
stub-app-projects/mp-harmony/**/cert-profile stub-app-projects/mp-harmony/**/cert-profile

View File

@@ -156,6 +156,14 @@ const localDevConfig = ({
uniacid: 248, uniacid: 248,
domain: 'https://5g.aigc-quickapp.com/', domain: 'https://5g.aigc-quickapp.com/',
}, },
'228': { // 商品分类,没有立即咨询文字
uniacid: 228,
domain: 'https://5g.aigc-quickapp.com/',
},
'2797': { //
uniacid: 2797,
domain: 'https://xcx.aigc-quickapp.com/',
},
// })['5g-237']; // 商品分类,没有立即咨询文字 // })['5g-237']; // 商品分类,没有立即咨询文字
// })['5g-97']; // 大宗商品贸易,支持自定义字体大小、颜色 // })['5g-97']; // 大宗商品贸易,支持自定义字体大小、颜色
// })['5g-50']; // 燃气安全产品机器人,购物车无反应 // })['5g-50']; // 燃气安全产品机器人,购物车无反应
@@ -169,7 +177,7 @@ const localDevConfig = ({
// })['2724']; // 生物菌肥 // })['2724']; // 生物菌肥
// })['2811']; // 选择要使用的环境配置 // })['2811']; // 选择要使用的环境配置
// })['local-11']; // 选择要使用的环境配置 // })['local-11']; // 选择要使用的环境配置
})['192.168.8.118-1']; // 本地 })['2797']; // 本地
// })['local-2']; // 本地 // })['local-2']; // 本地
// })['local-11']; // 本地 // })['local-11']; // 本地
// })['1-test']; // test // })['1-test']; // test

View File

@@ -102,6 +102,36 @@ export function parsePageParams(raw, defaultValue = {}) {
} }
/**
* 把任意值(通常是 URL 参数 / query string 解析出的字符串)强转为布尔值。
*
* 设计动机:URL query 中布尔值的合法写法不止一种——
* ?show_home=1 -> '1' -> true
* ?show_home=true -> 'true' -> true
* ?show_home=true -> true -> true
* ?show_home=0 -> '0' -> false
* ?show_home=false -> 'false' -> false
* ?show_home= -> '' -> false
* 未传 -> undefined -> false
*
* 与裸 `Boolean(x)` 的差异:
* Boolean('false') === true (任何非空字符串都是 true)
* Boolean('0') === true (同上)
* 本函数对这两个值返回 false,更适合 URL 参数解析场景。
*
* @param {*} val 任意值
* @returns {boolean}
*/
export function coerceBool(val) {
if (val === true || val === 1) return true;
if (typeof val === 'string') {
const s = val.trim().toLowerCase();
return s === '1' || s === 'true';
}
return false;
}
// 关于页面路径,使用常量导出 // 关于页面路径,使用常量导出
export const INDEX_PAGE_URL = '/pages/index/index'; export const INDEX_PAGE_URL = '/pages/index/index';
export const CATEGORY_PAGE_URL = '/pages_goods/category'; export const CATEGORY_PAGE_URL = '/pages_goods/category';
@@ -457,6 +487,9 @@ export default {
/** 把 page_params 还原成对象(JSON.parse 的安全封装) */ /** 把 page_params 还原成对象(JSON.parse 的安全封装) */
parsePageParams, parsePageParams,
/** 把任意值强转为布尔值,专治 URL query 参数布尔解析(?show_home=1/'true'/true) */
coerceBool,
/** HTML 实体解码 */ /** HTML 实体解码 */
decodeEntity, decodeEntity,

View File

@@ -76,6 +76,9 @@
</block> </block>
</view> </view>
</view> </view>
<view class="footer-wrap footer-wrap-template1">
<view class="detail-btn" @tap.stop="toDetail(item)">{{ $lang('components-diy.diy-category-item.consultNow') }}</view>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -110,6 +113,9 @@
</block> </block>
</view> </view>
</view> </view>
<view class="footer-wrap footer-wrap-template1">
<view class="detail-btn" @tap.stop="toDetail(item)">{{ $lang('components-diy.diy-category-item.consultNow') }}</view>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -173,6 +179,9 @@
</block> </block>
</view> </view>
</view> </view>
<view class="footer-wrap footer-wrap-template1">
<view class="detail-btn" @tap.stop="toDetail(item)">{{ $lang('components-diy.diy-category-item.consultNow') }}</view>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -1420,6 +1429,26 @@ export default {
border-radius: 12rpx; border-radius: 12rpx;
overflow: hidden; overflow: hidden;
margin-bottom: 20rpx; margin-bottom: 20rpx;
.footer-wrap-template1 {
padding: 16rpx 4rpx 0rpx;
display: flex;
justify-content: flex-end;
.detail-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0 32rpx;
height: 55rpx;
background-color: #ff4545;
color: #fff;
font-size: 25rpx;
border-radius: 26rpx;
box-sizing: border-box;
white-space: nowrap;
}
}
} }
} }
} }

View File

@@ -209,6 +209,10 @@ export default {
// 占位符:遗漏会被 ns-newform SCSS 默认值回退覆盖 // 占位符:遗漏会被 ns-newform SCSS 默认值回退覆盖
placeholderColor: pick('placeholderColor', 'placeholder_color'), placeholderColor: pick('placeholderColor', 'placeholder_color'),
placeholderFontSize: pick('placeholderFontSize', 'placeholder_font_size'), placeholderFontSize: pick('placeholderFontSize', 'placeholder_font_size'),
// textarea 显示行数 / 行高(透传给 ns-newform,与 ns-form 保持一致)
// 不传时 ns-newform 走自己的默认 4 行 / 44rpx
textareaRows: pick('textareaRows', 'textarea_rows'),
textareaLineHeight: pick('textareaLineHeight', 'textarea_line_height'),
// 输入框视觉增强(与 ns-newform 对齐) // 输入框视觉增强(与 ns-newform 对齐)
inputBorderColor: pick('inputBorderColor', 'input_border_color'), inputBorderColor: pick('inputBorderColor', 'input_border_color'),
inputBgColor: pick('inputBgColor', 'input_bg_color'), inputBgColor: pick('inputBgColor', 'input_bg_color'),

View File

@@ -51,7 +51,7 @@
<view v-if="showConsent" style="width:100%;box-sizing:border-box;margin-top:24rpx;background:#FFFFFF;border-radius:32rpx;padding:28rpx 28rpx;position:relative;overflow:hidden;box-shadow:0 4rpx 16rpx rgba(0,0,0,0.06);"> <view v-if="showConsent" style="width:100%;box-sizing:border-box;margin-top:24rpx;background:#FFFFFF;border-radius:32rpx;padding:28rpx 28rpx;position:relative;overflow:hidden;box-shadow:0 4rpx 16rpx rgba(0,0,0,0.06);">
<!-- 标题行 + 待获取标签 --> <!-- 标题行 + 待获取标签 -->
<view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;"> <view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;">
<text style="font-size:34rpx;font-weight:800;color:#1A1A1A;">{{ value.title || '我的推荐码' }}</text> <text :style="$util.objToStyle({ fontSize: px2rpx(value.titleFontSize || 17) + 'px', fontWeight: '800', color: value.titleColor || '#1A1A1A' })">{{ value.title || '我的推荐码' }}</text>
<view style="display:inline-flex;align-items:center;background:rgba(255,152,0,0.12);border-radius:16rpx;padding:4rpx 14rpx;gap:4rpx;"> <view style="display:inline-flex;align-items:center;background:rgba(255,152,0,0.12);border-radius:16rpx;padding:4rpx 14rpx;gap:4rpx;">
<text style="font-size:18rpx;color:#FF8F00;">✓</text> <text style="font-size:18rpx;color:#FF8F00;">✓</text>
<text style="font-size:20rpx;color:#FF8F00;font-weight:600;">待获取推荐码</text> <text style="font-size:20rpx;color:#FF8F00;font-weight:600;">待获取推荐码</text>
@@ -87,46 +87,57 @@
<!-- ============ 推荐码卡片:仅在非 consent 且非首次加载时渲染 ============ --> <!-- ============ 推荐码卡片:仅在非 consent 且非首次加载时渲染 ============ -->
<block v-if="!showConsent && !(loadingCode && !code)"> <block v-if="!showConsent && !(loadingCode && !code)">
<!-- ============ 预设 1:Gradient 渐变风格(默认)全部内联样式 ============ --> <!-- ============ 预设 1:Gradient 渐变风格(默认)全部内联样式 ============ -->
<view v-if="stylePreset === 'gradient'" style="width:100%;box-sizing:border-box;margin-top:24rpx;background:#FFFFFF;border-radius:32rpx;padding:28rpx 28rpx;position:relative;overflow:hidden;box-shadow:0 4rpx 16rpx rgba(0,0,0,0.06);"> <view v-if="stylePreset === 'gradient'" :style="$util.objToStyle({ width: '100%', boxSizing: 'border-box', marginTop: '24rpx', background: '#FFFFFF', borderRadius: px2rpx(value.borderRadius || 8) + 'px', padding: px2rpx(value.padding || 16) + 'px ' + px2rpx(value.padding || 16) + 'px', position: 'relative', overflow: 'hidden', boxShadow: '0 4rpx 16rpx rgba(0,0,0,0.06)' })">
<!-- 标题行 + 已获得推荐码标签 --> <!-- 标题行 + 已获得推荐码标签 -->
<view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;"> <view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;">
<text style="font-size:34rpx;font-weight:800;color:#1A1A1A;">{{ value.title || '我的推荐码' }}</text> <text :style="$util.objToStyle({ fontSize: px2rpx(value.titleFontSize || 17) + 'px', fontWeight: '800', color: value.titleColor || '#1A1A1A' })">{{ value.title || '我的推荐码' }}</text>
<view style="display:inline-flex;align-items:center;background:#52C41A;border-radius:16rpx;padding:4rpx 14rpx;gap:4rpx;"> <view style="display:inline-flex;align-items:center;background:#52C41A;border-radius:16rpx;padding:4rpx 14rpx;gap:4rpx;">
<text style="font-size:18rpx;color:#FFFFFF;">✓</text> <text style="font-size:18rpx;color:#FFFFFF;">✓</text>
<text style="font-size:20rpx;color:#FFFFFF;font-weight:600;">已获得推荐码</text> <text style="font-size:20rpx;color:#FFFFFF;font-weight:600;">已获得推荐码</text>
</view> </view>
</view> </view>
<!-- 推荐码+复制按钮区域:浅米黄色背景 --> <!-- 推荐码+复制按钮区域:浅米黄色背景,圆角紧凑 -->
<view style="margin-top:24rpx;background:#FFF8F0;border-radius:16rpx;padding:32rpx 20rpx 24rpx;position:relative;z-index:1;"> <view style="margin-top:24rpx;background:#FFF8F0;border-radius:16rpx;padding:32rpx 20rpx 24rpx;position:relative;z-index:1;">
<!-- 推荐码居中显示 --> <!-- 推荐码居中显示 -->
<view style="text-align:center;"> <view style="text-align:center;">
<text style="font-size:64rpx;font-weight:900;color:#DC143C;letter-spacing:2rpx;font-family:'Courier New',monospace;">{{ code || '暂无' }}</text> <text :style="$util.objToStyle({ fontSize: px2rpx(Math.max(16, Math.min(48, (Number(value.codeFontSize) || 28) * 0.93))) + 'px', fontWeight: '900', color: value.codeColor || '#DC143C', letterSpacing: '2rpx', fontFamily: 'Courier New, monospace' })">{{ code || value.placeholderCode || 'ABCD1234' }}</text>
</view> </view>
<!-- 复制推荐码按钮:白色小按钮,红边框,在推荐码下方居中 --> <!-- 复制推荐码按钮:白色小按钮,红边框,在推荐码下方居中 -->
<view style="margin-top:12rpx;display:flex;justify-content:center;"> <view v-if="value.showCopyButton !== false" style="margin-top:16rpx;text-align:center;">
<view @tap="copyRecommendCode" style="width:fit-content;height:60rpx;display:flex;justify-content:center;align-items:center;background:#FFFFFF;border:2rpx solid #DC143C;border-radius:30rpx;padding:0 32rpx;gap:8rpx;"> <view @tap="copyRecommendCode" :style="$util.objToStyle(copyBtnStyleObj({display:'inline-flex',alignItems:'center',borderRadius:'15px',padding:'0 16px',height:'30px',gap:'4px'}))">
<text style="font-size:22rpx;">📋</text> <text style="font-size:22rpx;">📋</text>
<text style="font-size:24rpx;color:#DC143C;font-weight:600;">复制推荐码</text> <text :style="$util.objToStyle(copyBtnStyleObj({}, true))">{{ nc.copyButtonText || '复制推荐码' }}</text>
</view> </view>
</view> </view>
</view> </view>
<!-- 立即分享链接按钮:黄橙渐变,小圆角,触发微信原生分享 --> <!-- 立即分享链接按钮:黄橙渐变,胶囊形圆角,紧凑布局
<button open-type="share" @tap="onShareClick" style="margin-top:20rpx;height:80rpx;display:flex;justify-content:center;align-items:center;background:linear-gradient(90deg,#FFC107 0%,#FF8F00 100%);border-radius:16rpx;box-shadow:0 4rpx 12rpx rgba(255,143,0,0.3);position:relative;z-index:1;border:none;line-height:1;"> 固定走 linear-gradient,不读 diyData.buttonBgColor(避免被后端默认色覆盖)
<text style="font-size:28rpx;font-weight:700;color:#FFFFFF;">📤 {{ value.buttonText || '立即分享' }}</text> H5 端用 view 触发(避免 button 默认样式),小程序/APP 端保留 button 以触发 open-type=share -->
<button v-if="value.showShareButton !== false && !isH5Platform" open-type="share" @tap="onShareClick" style="position:relative;left:0;right:0;margin:16rpx 0 0 0;width:100%;text-align:left;background:transparent;border:0;padding:0;line-height:normal;display:block;box-sizing:border-box;">
<view style="display:block;width:100%;height:80rpx;line-height:80rpx;background:linear-gradient(90deg,#FFC107 0%,#FF8F00 100%);border-radius:40rpx;box-shadow:0 4rpx 12rpx rgba(255,143,0,0.3);box-sizing:border-box;text-align:center;">
<text style="font-size:28rpx;font-weight:700;color:#FFFFFF;">📤 {{ value.buttonText || '立即分享' }}</text>
</view>
</button> </button>
<view v-else-if="value.showShareButton !== false" @tap="onShareClick" style="margin-top:16rpx;width:100%;">
<view style="display:block;height:80rpx;line-height:80rpx;background:linear-gradient(90deg,#FFC107 0%,#FF8F00 100%);border-radius:40rpx;box-shadow:0 4rpx 12rpx rgba(255,143,0,0.3);box-sizing:border-box;text-align:center;">
<text style="font-size:28rpx;font-weight:700;color:#FFFFFF;">📤 {{ nc.buttonText || '立即分享' }}</text>
</view>
</view>
<!-- showLink --> <!-- showLink -->
<view v-if="isFeatureOn(value.showLink)" style="margin-top:20rpx;text-align:center;position:relative;z-index:1;"> <view v-if="isFeatureOn(value.showLink)" style="margin-top:20rpx;text-align:center;position:relative;z-index:1;">
<text @tap="copyLink" style="font-size:22rpx;color:#999;text-decoration:underline;">{{ formatShareLink }}</text> <text @tap="copyLink" style="font-size:22rpx;color:#999;text-decoration:underline;">{{ formatShareLink }}</text>
</view> </view>
<!-- showQR --> <!-- showQR -->
<view v-if="isFeatureOn(value.showQR)" style="margin-top:20rpx;display:flex;flex-direction:column;align-items:center;position:relative;z-index:1;"> <view v-if="isFeatureOn(value.showQR)" style="margin-top:20rpx;display:flex;flex-direction:column;align-items:center;position:relative;z-index:1;">
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:200rpx;height:200rpx;" /> <view :style="$util.objToStyle({ width: px2rpx(nc.qrSize || 200) + 'px', height: px2rpx(nc.qrSize || 200) + 'px', background: nc.qrBoxBackground || 'transparent', border: nc.qrBoxBorderWidth && nc.qrBoxBorderColor ? (px2rpx(nc.qrBoxBorderWidth) + 'px solid ' + nc.qrBoxBorderColor) : 'none', borderRadius: px2rpx(nc.qrBoxBorderRadius || 0) + 'px', padding: px2rpx(nc.qrBoxPadding || 0) + 'px', boxSizing: 'border-box' })">
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:100%;height:100%;display:block;" />
</view>
<text style="font-size:22rpx;color:#999;margin-top:12rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text> <text style="font-size:22rpx;color:#999;margin-top:12rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text>
</view> </view>
</view> </view>
<!-- ============ 预设 2:Minimal 极简风格(全部内联样式) ============ --> <!-- ============ 预设 2:Minimal 极简风格(全部内联样式) ============ -->
<view v-if="stylePreset === 'minimal'" style="width:100%;box-sizing:border-box;margin-top:24rpx;background:linear-gradient(90deg,#FFFFFF 0%,#FFF8F8 30%,#FFF5F5 60%,#FFF0F0 100%);border-radius:32rpx;padding:28rpx 28rpx;position:relative;overflow:hidden;box-shadow:0 4rpx 16rpx rgba(255,107,107,0.08);"> <view v-if="stylePreset === 'minimal'" :style="$util.objToStyle({ width: '100%', boxSizing: 'border-box', marginTop: '24rpx', background: 'linear-gradient(90deg,#FFFFFF 0%,#FFF8F8 30%,#FFF5F5 60%,#FFF0F0 100%)', borderRadius: px2rpx(value.borderRadius || 8) + 'px', padding: px2rpx(value.padding || 16) + 'px ' + px2rpx(value.padding || 16) + 'px', position: 'relative', overflow: 'hidden', boxShadow: '0 4rpx 16rpx rgba(255,107,107,0.08)' })">
<!-- 背景礼物装饰大图1(右上角,移至顶部) --> <!-- 背景礼物装饰大图1(右上角,移至顶部) -->
<view style="position:absolute;top:-20rpx;right:-30rpx;z-index:0;"> <view style="position:absolute;top:-20rpx;right:-30rpx;z-index:0;">
<text style="font-size:200rpx;opacity:0.15;">🎁</text> <text style="font-size:200rpx;opacity:0.15;">🎁</text>
@@ -147,7 +158,7 @@
</view> </view>
<!-- 标题行 + 已获得标签 --> <!-- 标题行 + 已获得标签 -->
<view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;"> <view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;">
<text style="font-size:34rpx;font-weight:800;color:#1A1A1A;">{{ value.title || '我的推荐码' }}</text> <text :style="$util.objToStyle({ fontSize: px2rpx(value.titleFontSize || 17) + 'px', fontWeight: '800', color: value.titleColor || '#1A1A1A' })">{{ value.title || '我的推荐码' }}</text>
<view style="display:inline-flex;align-items:center;background:rgba(255,107,107,0.15);border-radius:16rpx;padding:4rpx 14rpx;"> <view style="display:inline-flex;align-items:center;background:rgba(255,107,107,0.15);border-radius:16rpx;padding:4rpx 14rpx;">
<text style="font-size:20rpx;color:#FF6B6B;font-weight:600;">已获得</text> <text style="font-size:20rpx;color:#FF6B6B;font-weight:600;">已获得</text>
</view> </view>
@@ -159,40 +170,51 @@
<!-- 推荐码红色虚线框:推荐码居中,圆角更圆,更紧凑 --> <!-- 推荐码红色虚线框:推荐码居中,圆角更圆,更紧凑 -->
<view style="margin-top:20rpx;background:rgba(255,255,255,0.7);border:2rpx dashed #FF6B6B;border-radius:32rpx;padding:12rpx 16rpx;position:relative;z-index:1;"> <view style="margin-top:20rpx;background:rgba(255,255,255,0.7);border:2rpx dashed #FF6B6B;border-radius:32rpx;padding:12rpx 16rpx;position:relative;z-index:1;">
<view style="text-align:center;"> <view style="text-align:center;">
<text style="font-size:56rpx;font-weight:800;color:#FF4444;letter-spacing:2rpx;font-family:'Courier New',monospace;">{{ code || '暂无' }}</text> <text :style="$util.objToStyle({ fontSize: px2rpx(Math.max(16, Math.min(48, (Number(value.codeFontSize) || 28) * 0.86))) + 'px', fontWeight: '800', color: value.codeColor || '#FF4444', letterSpacing: '2rpx', fontFamily: 'Courier New, monospace' })">{{ code || value.placeholderCode || 'ABCD1234' }}</text>
</view> </view>
</view> </view>
<!-- 复制推荐码按钮:白色小按钮,在虚线框下方、立即分享链接上方 --> <!-- 复制推荐码按钮:白色小按钮,在虚线框下方、立即分享链接上方 -->
<view style="margin-top:14rpx;display:flex;justify-content:center;position:relative;z-index:1;"> <view v-if="value.showCopyButton !== false" style="margin-top:14rpx;display:flex;justify-content:center;position:relative;z-index:1;">
<view @tap="copyRecommendCode" style="width:fit-content;height:60rpx;display:flex;justify-content:center;align-items:center;background:#FFFFFF;border:2rpx solid #FF6B6B;border-radius:30rpx;padding:0 32rpx;gap:8rpx;"> <view @tap="copyRecommendCode" :style="$util.objToStyle(copyBtnStyleObj({width:'fit-content',height:'30px',display:'flex',justifyContent:'center',alignItems:'center',borderRadius:'15px',padding:'0 16px',gap:'4px'}))">
<text style="font-size:22rpx;">📋</text> <text style="font-size:22rpx;">📋</text>
<text style="font-size:24rpx;color:#FF6B6B;font-weight:600;">复制推荐码</text> <text :style="$util.objToStyle(copyBtnStyleObj({}, true))">{{ nc.copyButtonText || '复制推荐码' }}</text>
</view> </view>
</view> </view>
<!-- 立即分享链接按钮:红色背景全宽铺满,触发微信原生分享 --> <!-- 立即分享链接按钮:红橙渐变胶囊全宽铺满,触发微信原生分享
<button open-type="share" @tap="onShareClick" style="margin-top:16rpx;height:76rpx;display:flex;justify-content:center;align-items:center;background:linear-gradient(90deg,#FF6B6B 0%,#FF4444 100%);border-radius:38rpx;box-shadow:0 4rpx 12rpx rgba(255,68,68,0.3);position:relative;z-index:1;border:none;line-height:1;"> 固定走 linear-gradient,不读 diyData.buttonBgColor
<text style="font-size:27rpx;font-weight:700;color:#FFFFFF;">📤 {{ value.buttonText || '立即分享' }}</text> H5 端用 view 触发(避免 button 默认样式),小程序/APP 端保留 button 触发 open-type=share -->
<button v-if="value.showShareButton !== false && !isH5Platform" open-type="share" @tap="onShareClick" style="position:relative;left:0;right:0;margin:16rpx 0 0 0;width:100%;text-align:left;background:transparent;border:0;padding:0;line-height:normal;display:block;box-sizing:border-box;">
<view style="display:block;width:100%;height:76rpx;line-height:76rpx;background:linear-gradient(90deg,#FF6B6B 0%,#FF4444 100%);border-radius:38rpx;box-shadow:0 4rpx 12rpx rgba(255,68,68,0.3);box-sizing:border-box;text-align:center;">
<text style="font-size:27rpx;font-weight:700;color:#FFFFFF;">📤 {{ value.buttonText || '立即分享' }}</text>
</view>
</button> </button>
<view v-else-if="value.showShareButton !== false" @tap="onShareClick" style="margin-top:16rpx;width:100%;">
<view style="display:block;width:100%;height:76rpx;line-height:76rpx;background:linear-gradient(90deg,#FF6B6B 0%,#FF4444 100%);border-radius:38rpx;box-shadow:0 4rpx 12rpx rgba(255,68,68,0.3);box-sizing:border-box;text-align:center;">
<text style="font-size:27rpx;font-weight:700;color:#FFFFFF;">📤 {{ nc.buttonText || '立即分享' }}</text>
</view>
</view>
<!-- showLink --> <!-- showLink -->
<view v-if="isFeatureOn(value.showLink)" style="margin-top:16rpx;text-align:center;position:relative;z-index:1;"> <view v-if="isFeatureOn(value.showLink)" style="margin-top:16rpx;text-align:center;position:relative;z-index:1;">
<text @tap="copyLink" style="font-size:22rpx;color:#999;text-decoration:underline;">{{ formatShareLink }}</text> <text @tap="copyLink" style="font-size:22rpx;color:#999;text-decoration:underline;">{{ formatShareLink }}</text>
</view> </view>
<!-- showQR --> <!-- showQR -->
<view v-if="isFeatureOn(value.showQR)" style="margin-top:16rpx;display:flex;flex-direction:column;align-items:center;position:relative;z-index:1;"> <view v-if="isFeatureOn(value.showQR)" style="margin-top:16rpx;display:flex;flex-direction:column;align-items:center;position:relative;z-index:1;">
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:200rpx;height:200rpx;" /> <view :style="$util.objToStyle({ width: px2rpx(nc.qrSize || 200) + 'px', height: px2rpx(nc.qrSize || 200) + 'px', background: nc.qrBoxBackground || 'transparent', border: nc.qrBoxBorderWidth && nc.qrBoxBorderColor ? (px2rpx(nc.qrBoxBorderWidth) + 'px solid ' + nc.qrBoxBorderColor) : 'none', borderRadius: px2rpx(nc.qrBoxBorderRadius || 0) + 'px', padding: px2rpx(nc.qrBoxPadding || 0) + 'px', boxSizing: 'border-box' })">
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:100%;height:100%;display:block;" />
</view>
<text style="font-size:22rpx;color:#999;margin-top:8rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text> <text style="font-size:22rpx;color:#999;margin-top:8rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text>
</view> </view>
</view> </view>
<!-- ============ 预设 3:Card 卡片风格(全部内联样式) ============ --> <!-- ============ 预设 3:Card 卡片风格(全部内联样式) ============ -->
<view v-if="stylePreset === 'card'" style="width:100%;box-sizing:border-box;margin-top:24rpx;background:linear-gradient(180deg,#FFF9F5 0%,#FFF5F0 100%);border-radius:32rpx;padding:28rpx 28rpx;position:relative;overflow:hidden;"> <view v-if="stylePreset === 'card'" :style="$util.objToStyle({ width: '100%', boxSizing: 'border-box', marginTop: '24rpx', background: 'linear-gradient(180deg,#FFF9F5 0%,#FFF5F0 100%)', borderRadius: px2rpx(value.borderRadius || 8) + 'px', padding: px2rpx(value.padding || 16) + 'px ' + px2rpx(value.padding || 16) + 'px', position: 'relative', overflow: 'hidden' })">
<!-- 右上角礼物装饰(开盖礼物盒) --> <!-- 右上角礼物装饰(开盖礼物盒) -->
<view style="position:absolute;top:16rpx;right:20rpx;z-index:2;"> <view style="position:absolute;top:16rpx;right:20rpx;z-index:2;">
<text style="font-size:52rpx;">🎁</text> <text style="font-size:52rpx;">🎁</text>
</view> </view>
<!-- 标题行 + 已生效标签 --> <!-- 标题行 + 已生效标签 -->
<view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;"> <view style="display:flex;align-items:center;gap:12rpx;position:relative;z-index:1;">
<text style="font-size:34rpx;font-weight:800;color:#1A1A1A;">{{ value.title || '我的推荐码' }}</text> <text :style="$util.objToStyle({ fontSize: px2rpx(value.titleFontSize || 17) + 'px', fontWeight: '800', color: value.titleColor || '#1A1A1A' })">{{ value.title || '我的推荐码' }}</text>
<view style="display:inline-flex;align-items:center;background:rgba(255,107,107,0.12);border-radius:16rpx;padding:4rpx 14rpx;"> <view style="display:inline-flex;align-items:center;background:rgba(255,107,107,0.12);border-radius:16rpx;padding:4rpx 14rpx;">
<text style="font-size:20rpx;color:#FF6B6B;font-weight:600;">✓ 已生效</text> <text style="font-size:20rpx;color:#FF6B6B;font-weight:600;">✓ 已生效</text>
</view> </view>
@@ -204,35 +226,45 @@
<!-- 推荐码红色虚线框:居中,圆角,紧凑 --> <!-- 推荐码红色虚线框:居中,圆角,紧凑 -->
<view style="margin-top:20rpx;display:flex;justify-content:center;position:relative;z-index:1;"> <view style="margin-top:20rpx;display:flex;justify-content:center;position:relative;z-index:1;">
<view style="background:rgba(255,255,255,0.7);border:2rpx dashed #FF6B6B;border-radius:32rpx;padding:12rpx 32rpx;text-align:center;"> <view style="background:rgba(255,255,255,0.7);border:2rpx dashed #FF6B6B;border-radius:32rpx;padding:12rpx 32rpx;text-align:center;">
<text style="font-size:48rpx;font-weight:800;color:#FF4444;letter-spacing:2rpx;font-family:'Courier New',monospace;">{{ code || '暂无' }}</text> <text :style="$util.objToStyle({ fontSize: px2rpx(Math.max(16, Math.min(48, (Number(value.codeFontSize) || 28) * 0.71))) + 'px', fontWeight: '800', color: value.codeColor || '#FF4444', letterSpacing: '2rpx', fontFamily: 'Courier New, monospace' })">{{ code || value.placeholderCode || 'ABCD1234' }}</text>
</view> </view>
</view> </view>
<!-- 复制推荐码按钮:白色小按钮,在虚线框下方、立即分享链接上方,居中 --> <!-- 复制推荐码按钮:白色小按钮,在虚线框下方、立即分享链接上方,居中 -->
<view style="margin-top:14rpx;display:flex;justify-content:center;position:relative;z-index:1;"> <view v-if="value.showCopyButton !== false" style="margin-top:14rpx;display:flex;justify-content:center;position:relative;z-index:1;">
<view @tap="copyRecommendCode" style="width:fit-content;height:60rpx;display:flex;justify-content:center;align-items:center;background:#FFFFFF;border:2rpx solid #FF6B6B;border-radius:30rpx;padding:0 32rpx;gap:8rpx;"> <view @tap="copyRecommendCode" :style="$util.objToStyle(copyBtnStyleObj({width:'fit-content',height:'30px',display:'flex',justifyContent:'center',alignItems:'center',borderRadius:'15px',padding:'0 16px',gap:'4px'}))">
<text style="font-size:22rpx;">📋</text> <text style="font-size:22rpx;">📋</text>
<text style="font-size:24rpx;color:#FF6B6B;font-weight:600;">复制推荐码</text> <text :style="$util.objToStyle(copyBtnStyleObj({}, true))">{{ nc.copyButtonText || '复制推荐码' }}</text>
</view> </view>
</view> </view>
<!-- 立即分享链接按钮:全宽铺满白底红边框,触发微信原生分享 --> <!-- 立即分享链接按钮:Card 风格固定「白底红边框」设计,不读 buttonBgColor
<button open-type="share" @tap="onShareClick" style="margin-top:16rpx;height:76rpx;display:flex;justify-content:center;align-items:center;background:#FFFFFF;border:2rpx solid #FF6B6B;border-radius:38rpx;position:relative;z-index:1;line-height:1;"> H5 端改用 view 触发(避免 <button> 默认样式影响),小程序/APP 端保留 button 以触发 open-type=share -->
<text style="font-size:27rpx;font-weight:600;color:#FF6B6B;">📤 {{ value.buttonText || '立即分享' }}</text> <button v-if="value.showShareButton !== false && !isH5Platform" open-type="share" @tap="onShareClick" style="position:relative;left:0;right:0;margin:16rpx 0 0 0;width:100%;text-align:left;background:transparent;border:0;padding:0;line-height:normal;display:block;box-sizing:border-box;">
<view style="display:block;width:100%;height:76rpx;line-height:76rpx;background:#FFFFFF;border:2rpx solid #FF6B6B;border-radius:38rpx;box-sizing:border-box;text-align:center;">
<text style="font-size:27rpx;font-weight:600;color:#FF6B6B;">📤 {{ value.buttonText || '立即分享' }}</text>
</view>
</button> </button>
<view v-else-if="value.showShareButton !== false" @tap="onShareClick" style="margin-top:16rpx;width:100%;">
<view style="display:block;width:100%;height:76rpx;line-height:76rpx;background:#FFFFFF;border:2rpx solid #FF6B6B;border-radius:38rpx;box-sizing:border-box;text-align:center;">
<text style="font-size:27rpx;font-weight:600;color:#FF6B6B;">📤 {{ nc.buttonText || '立即分享' }}</text>
</view>
</view>
<!-- showLink --> <!-- showLink -->
<view v-if="isFeatureOn(value.showLink)" style="margin-top:16rpx;text-align:center;position:relative;z-index:1;"> <view v-if="isFeatureOn(value.showLink)" style="margin-top:16rpx;text-align:center;position:relative;z-index:1;">
<text @tap="copyLink" style="font-size:22rpx;color:#999;text-decoration:underline;">{{ formatShareLink }}</text> <text @tap="copyLink" style="font-size:22rpx;color:#999;text-decoration:underline;">{{ formatShareLink }}</text>
</view> </view>
<!-- showQR --> <!-- showQR -->
<view v-if="isFeatureOn(value.showQR)" style="margin-top:16rpx;display:flex;flex-direction:column;align-items:center;position:relative;z-index:1;"> <view v-if="isFeatureOn(value.showQR)" style="margin-top:16rpx;display:flex;flex-direction:column;align-items:center;position:relative;z-index:1;">
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:200rpx;height:200rpx;" /> <view :style="$util.objToStyle({ width: px2rpx(nc.qrSize || 200) + 'px', height: px2rpx(nc.qrSize || 200) + 'px', background: nc.qrBoxBackground || 'transparent', border: nc.qrBoxBorderWidth && nc.qrBoxBorderColor ? (px2rpx(nc.qrBoxBorderWidth) + 'px solid ' + nc.qrBoxBorderColor) : 'none', borderRadius: px2rpx(nc.qrBoxBorderRadius || 0) + 'px', padding: px2rpx(nc.qrBoxPadding || 0) + 'px', boxSizing: 'border-box' })">
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:100%;height:100%;display:block;" />
</view>
<text style="font-size:22rpx;color:#999;margin-top:8rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text> <text style="font-size:22rpx;color:#999;margin-top:8rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text>
</view> </view>
</view> </view>
<!-- ============ 预设 4:Horizontal 横向风格(全部内联样式,匹配参考图) ============ --> <!-- ============ 预设 4:Horizontal 横向风格(全部内联样式,匹配参考图) ============ -->
<view v-if="stylePreset === 'horizontal'" style="width:100%;box-sizing:border-box;margin-top:24rpx;background:#FFFFFF;border-radius:32rpx;padding:28rpx 28rpx;position:relative;overflow:hidden;box-shadow:0 4rpx 16rpx rgba(0,0,0,0.06);"> <view v-if="stylePreset === 'horizontal'" :style="$util.objToStyle({ width: '100%', boxSizing: 'border-box', marginTop: '24rpx', background: '#FFFFFF', borderRadius: px2rpx(value.borderRadius || 8) + 'px', padding: px2rpx(value.padding || 16) + 'px ' + px2rpx(value.padding || 16) + 'px', position: 'relative', overflow: 'hidden', boxShadow: '0 4rpx 16rpx rgba(0,0,0,0.06)' })">
<!-- 卡片顶部:主标题 --> <!-- 卡片顶部:主标题 -->
<text style="font-size:34rpx;font-weight:800;color:#1A1A1A;display:block;">{{ value.title || '我的推荐码' }}</text> <text :style="$util.objToStyle({ fontSize: px2rpx(value.titleFontSize || 17) + 'px', fontWeight: '800', color: value.titleColor || '#1A1A1A', display: 'block' })">{{ value.title || '我的推荐码' }}</text>
<!-- 内容主体框:浅橙细边框,极浅米白底色 --> <!-- 内容主体框:浅橙细边框,极浅米白底色 -->
<view style="margin-top:20rpx;background:#FFFCF8;border:2rpx solid #FFE0B2;border-radius:20rpx;padding:24rpx 24rpx 20rpx;position:relative;overflow:hidden;"> <view style="margin-top:20rpx;background:#FFFCF8;border:2rpx solid #FFE0B2;border-radius:20rpx;padding:24rpx 24rpx 20rpx;position:relative;overflow:hidden;">
<!-- 2.1 状态标签(左上角) --> <!-- 2.1 状态标签(左上角) -->
@@ -252,23 +284,29 @@
</view> </view>
<!-- 2.3 推荐码核心文本(左对齐) --> <!-- 2.3 推荐码核心文本(左对齐) -->
<view style="margin-top:20rpx;position:relative;z-index:1;"> <view style="margin-top:20rpx;position:relative;z-index:1;">
<text style="font-size:72rpx;font-weight:900;color:#FF4444;letter-spacing:2rpx;font-family:'Courier New',monospace;">{{ code || '暂无' }}</text> <text :style="$util.objToStyle({ fontSize: px2rpx(Math.max(16, Math.min(48, (Number(value.codeFontSize) || 28) * 1.07))) + 'px', fontWeight: '900', color: value.codeColor || '#FF4444', letterSpacing: '2rpx', fontFamily: 'Courier New, monospace' })">{{ code || value.placeholderCode || 'ABCD1234' }}</text>
</view> </view>
<!-- 2.4 + 2.5 按钮行:左复制、右分享,上下错落 --> <!-- 2.4 + 2.5 按钮行:左复制、右分享,按内容自适应宽度,两端对称对齐 -->
<view style="margin-top:16rpx;display:flex;align-items:flex-end;justify-content:space-between;gap:16rpx;position:relative;z-index:1;"> <view style="margin-top:16rpx;display:flex;align-items:center;justify-content:space-between;gap:16rpx;padding:0 8rpx;position:relative;z-index:1;">
<!-- 左下角:复制推荐码按钮(描边圆角胶囊) --> <!-- 左下角:复制推荐码按钮(描边圆角胶囊,按内容自适应) -->
<view @tap="copyRecommendCode" style="height:64rpx;display:flex;justify-content:center;align-items:center;background:#FFFFFF;border:2rpx solid #FFB74D;border-radius:32rpx;padding:0 24rpx;gap:6rpx;"> <view v-if="value.showCopyButton !== false" @tap="copyRecommendCode" :style="$util.objToStyle(copyBtnStyleObj({height:'32px',display:'inline-flex',justifyContent:'center',alignItems:'center',borderRadius:'16px',padding:'0 12px',gap:'3px'}))">
<text style="font-size:22rpx;">📋</text> <text style="font-size:22rpx;">📋</text>
<text style="font-size:24rpx;color:#FF8F00;font-weight:600;">复制推荐码</text> <text :style="$util.objToStyle(copyBtnStyleObj({}, true))">{{ nc.copyButtonText || '复制推荐码' }}</text>
</view> </view>
<!-- 右下角:立即分享链接按钮(渐变橙色圆角胶囊,靠右,触发微信原生分享) --> <!-- 右下角:立即分享链接按钮(黄橙渐变胶囊,按内容自适应,触发微信原生分享)
<button open-type="share" @tap="onShareClick" style="height:72rpx;display:flex;justify-content:center;align-items:center;background:linear-gradient(90deg,#FF9800 0%,#FF4444 100%);border-radius:36rpx;padding:0 40rpx;box-shadow:0 4rpx 12rpx rgba(255,68,68,0.25);margin-bottom:-4rpx;border:none;line-height:1;"> H5 端用 view 触发,小程序/APP 端保留 button 以触发 open-type=share -->
<text style="font-size:27rpx;font-weight:700;color:#FFFFFF;">📤 {{ value.buttonText || '立即分享' }}</text> <button v-if="value.showShareButton !== false && !isH5Platform" open-type="share" @tap="onShareClick" style="position:relative;left:0;right:0;margin:0;padding:0;text-align:center;background:transparent;border:0;line-height:normal;display:inline-block;">
<view style="display:inline-block;height:72rpx;line-height:72rpx;padding:0 32rpx;background:linear-gradient(90deg,#FFC107 0%,#FF8F00 100%);border-radius:36rpx;box-shadow:0 4rpx 12rpx rgba(255,143,0,0.3);box-sizing:border-box;">
<text style="font-size:27rpx;font-weight:700;color:#FFFFFF;">📤 {{ value.buttonText || '立即分享' }}</text>
</view>
</button> </button>
<view v-else-if="value.showShareButton !== false" @tap="onShareClick" style="height:72rpx;display:inline-flex;justify-content:center;align-items:center;background:linear-gradient(90deg,#FFC107 0%,#FF8F00 100%);border-radius:36rpx;box-shadow:0 4rpx 12rpx rgba(255,143,0,0.3);padding:0 32rpx;line-height:1;box-sizing:border-box;">
<text style="font-size:27rpx;font-weight:700;color:#FFFFFF;">📤 {{ nc.buttonText || '立即分享' }}</text>
</view>
</view> </view>
</view> </view>
<!-- 换一个新码 --> <!-- 换一个新码 -->
<view v-if="isFeatureOn(value.allowRegenerate)" style="margin-top:16rpx;display:flex;justify-content:center;"> <view v-if="value.allowRegenerate === true" style="margin-top:16rpx;display:flex;justify-content:center;">
<view @tap="regenerate" style="background:rgba(255,68,68,0.06);border-radius:24rpx;padding:8rpx 24rpx;"> <view @tap="regenerate" style="background:rgba(255,68,68,0.06);border-radius:24rpx;padding:8rpx 24rpx;">
<text style="font-size:22rpx;color:#FF4444;font-weight:600;">{{ value.placeholderRegenerateText || '换一个新码' }}</text> <text style="font-size:22rpx;color:#FF4444;font-weight:600;">{{ value.placeholderRegenerateText || '换一个新码' }}</text>
</view> </view>
@@ -279,7 +317,9 @@
</view> </view>
<!-- showQR --> <!-- showQR -->
<view v-if="isFeatureOn(value.showQR)" style="margin-top:16rpx;display:flex;flex-direction:column;align-items:center;"> <view v-if="isFeatureOn(value.showQR)" style="margin-top:16rpx;display:flex;flex-direction:column;align-items:center;">
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:200rpx;height:200rpx;" /> <view :style="$util.objToStyle({ width: px2rpx(nc.qrSize || 200) + 'px', height: px2rpx(nc.qrSize || 200) + 'px', background: nc.qrBoxBackground || 'transparent', border: nc.qrBoxBorderWidth && nc.qrBoxBorderColor ? (px2rpx(nc.qrBoxBorderWidth) + 'px solid ' + nc.qrBoxBorderColor) : 'none', borderRadius: px2rpx(nc.qrBoxBorderRadius || 0) + 'px', padding: px2rpx(nc.qrBoxPadding || 0) + 'px', boxSizing: 'border-box' })">
<image :src="qrcodeRenderUrl" mode="aspectFit" style="width:100%;height:100%;display:block;" />
</view>
<text style="font-size:22rpx;color:#999;margin-top:8rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text> <text style="font-size:22rpx;color:#999;margin-top:8rpx;">{{ value.posterQrTip || '扫码进入并填写入驻信息' }}</text>
</view> </view>
</view> </view>
@@ -287,6 +327,9 @@
</view> </view>
<!-- 登录组件占位(ns-login 全局 mixin 已注册) --> <!-- 登录组件占位(ns-login 全局 mixin 已注册) -->
<ns-login ref="nsLoginBox" :popup="true" @getUserInfo="onLoginSuccess"></ns-login> <ns-login ref="nsLoginBox" :popup="true" @getUserInfo="onLoginSuccess"></ns-login>
<!-- ============ 分享海报 canvas(隐藏,仅小程序分享时生成推荐码组件的截图) ============ -->
<canvas v-if="!isH5Platform" canvas-id="sharePosterCanvas" id="sharePosterCanvas"
style="position:fixed;left:-9999px;top:-9999px;width:375px;height:480px;z-index:-1;"></canvas>
</view> </view>
</template> </template>
@@ -349,11 +392,12 @@ export default {
/* ============== 内容配置,diy_view.php 默认值对齐) ============== */ /* ============== 内容配置,diy_view.php 默认值对齐) ============== */
title: '我的推荐码', title: '我的推荐码',
buttonText: '立即分享', buttonText: '立即分享',
copyButtonText: '复制推荐码',
showButton: true, showButton: true,
showShareButton: true,
showCopyButton: true,
allowRegenerate: true, allowRegenerate: true,
placeholderRegenerateText: '换一个新码', placeholderRegenerateText: '换一个新码',
placeholderCopyText: '复制链接',
placeholderShareText: '分享',
requireConsent: true, requireConsent: true,
consentText: '您的推荐码将用于企业入驻来源追踪,我们会记录每一次通过您分享链接完成的入驻申请,用于您的推广收益结算。', consentText: '您的推荐码将用于企业入驻来源追踪,我们会记录每一次通过您分享链接完成的入驻申请,用于您的推广收益结算。',
consentButtonText: '同意并获取推荐码', consentButtonText: '同意并获取推荐码',
@@ -366,9 +410,9 @@ export default {
borderColor: '#E5E7EB', borderColor: '#E5E7EB',
accentColor: '#FFD700', accentColor: '#FFD700',
textColor: '#FFFFFF', textColor: '#FFFFFF',
titleColor: '#333333', titleColor: '#1A1A1A',
codeColor: '#E4393C', codeColor: '#E4393C',
buttonBgColor: '#E4393C', buttonBgColor: '', // 默认空:让 Card 风格的立即分享按钮走「白底红边框」分支,匹配设计参考图
buttonTextColor: '#FFFFFF', buttonTextColor: '#FFFFFF',
shareButtonBgColor: '#67C23A', shareButtonBgColor: '#67C23A',
titleFontSize: 14, titleFontSize: 14,
@@ -377,6 +421,13 @@ export default {
padding: 16, padding: 16,
showQR: false, showQR: false,
showLink: false, showLink: false,
// 二维码容器样式(与后端 diyData / design.html 对齐)
qrSize: 200, // 二维码图片尺寸(前端 rpx)
qrBoxBorderColor: '', // 容器边框色;空=不渲染边框
qrBoxBorderWidth: 0, // 边框宽度(rpx)
qrBoxBorderRadius: 0, // 容器圆角(rpx)
qrBoxBackground: '', // 容器背景色;空=透明
qrBoxPadding: 0, // 容器内边距(rpx)
/* ============== 兼容旧字段(不破坏之前商家已经配置过的卡片) ============== */ /* ============== 兼容旧字段(不破坏之前商家已经配置过的卡片) ============== */
// 外间距 // 外间距
@@ -395,6 +446,8 @@ export default {
code: '', code: '',
shareLink: '', shareLink: '',
qrcodeUrl: '', qrcodeUrl: '',
/* H5 平台标识:用于在 H5 端用 view 替代 button,避免浏览器 <button> 默认样式覆盖 */
isH5Platform: false,
/* 状态机 */ /* 状态机 */
showConsent: false, showConsent: false,
loadingCode: false, loadingCode: false,
@@ -636,6 +689,12 @@ export default {
}, },
created() { created() {
try { try {
// H5 端检测:避开 <button> 默认样式影响(h5 端改用 view 触发)
try {
// #ifdef H5
this.isH5Platform = true;
// #endif
} catch (e) {}
if (this.storeToken) { if (this.storeToken) {
this._resetAll(); this._resetAll();
var vm = this; var vm = this;
@@ -676,12 +735,37 @@ export default {
onShareAppMessage() { onShareAppMessage() {
const v = this.nc; const v = this.nc;
const title = v.shareTitle || v.title || '邀您入驻低空经济产业平台'; const title = v.shareTitle || v.title || '邀您入驻低空经济产业平台';
// 小程序环境下,生成「仅推荐码组件」的截图海报作为分享图
// H5 平台不进入 canvas 路径,直接用 diyData.shareImageUrl
if (!this.isH5Platform) {
return new Promise((resolve) => {
// 先生成海报;失败或非小程序时回退到默认 imageUrl
this.generateSharePoster()
.then((posterPath) => {
resolve({
title,
path: this.extractMiniProgramPath(),
imageUrl: posterPath || (v.shareImageUrl || '')
});
})
.catch(() => {
resolve({
title,
path: this.extractMiniProgramPath(),
imageUrl: v.shareImageUrl || ''
});
});
});
}
return { return {
title, title,
path: this.extractMiniProgramPath(), path: this.extractMiniProgramPath(),
imageUrl: v.shareImageUrl || '' imageUrl: v.shareImageUrl || ''
}; };
}, },
/**
* 小程序分享到朋友圈(uniapp 生命周期钩子,必须在 export default 对象上,不能进 methods)
*/
onShareTimeline() { onShareTimeline() {
const v = this.nc; const v = this.nc;
// 从H5分享链接中提取完整查询参数,与分享给好友保持一致 // 从H5分享链接中提取完整查询参数,与分享给好友保持一致
@@ -717,8 +801,39 @@ export default {
'font-weight:700;font-size:26rpx;' 'font-weight:700;font-size:26rpx;'
].join(''); ].join('');
}, },
/**
* 复制按钮样式对象(跟随后端 diyData.codeColor)
* 返回对象,用 $util.objToStyle 转字符串,
* 兼容 H5/小程序/APP/鸿蒙
* 注意:使用 px 而非 rpx 单位,避免鸿蒙元服务对动态 style 中 rpx
* 解析异常的问题。1rpx = 0.5px,直接换算成 px 数值。
* @param {Object} preset 预设差异(可选,如 {borderRadius:'16px',height:'32px'})
* @param {Boolean} textOnly 只返回文字样式(用于内嵌 text 节点)
*/
copyBtnStyleObj(preset, textOnly) {
const v = this.nc;
const codeColor = (v && v.codeColor) ? v.codeColor : '#DC143C';
if (textOnly) {
return Object.assign({
fontSize: '12px', // 原 24rpx → 12px
color: codeColor,
fontWeight: '600'
}, preset || {});
}
const base = {
background: '#FFFFFF',
border: `1px solid ${codeColor}`, // 原 2rpx → 1px
boxSizing: 'border-box'
};
return Object.assign(base, preset || {});
},
px2rpx(val) { px2rpx(val) {
return Number(val || 0) * 2 // 后端 diyData 数值语义是 px(商家在设计器按 px 输入,后端 wap/design.html
// 也都用 px 输出)。前端之前 *2 转 rpx 是错的,商家设置 8px 实际渲染
// 成 16px 偏大。鸿蒙元服务对动态拼接的 rpx 也有解析问题,所以这里
// 直接返回原值,作为 px 使用。
// 调用方:动态拼接时用 + 'px'(不要用 + 'px')。
return Number(val || 0)
}, },
isFeatureOn(v) { isFeatureOn(v) {
if (v === false || v === 0 || v === '0' || v === 'false' || v === null || v === undefined) return false; if (v === false || v === 0 || v === '0' || v === 'false' || v === null || v === undefined) return false;
@@ -877,7 +992,9 @@ export default {
if (consent) { if (consent) {
uni.showToast && uni.showToast({ title: '获取成功', icon: 'success' }); uni.showToast && uni.showToast({ title: '获取成功', icon: 'success' });
} }
// 拿到码后如果 showQR=true,顺便调 getMyQrcode(占位接口) // 拿到码后立即预生成分享海报(不等二维码,先占位;二维码拿到后会再次更新)
try { vm._prepareSharePoster(); } catch(e){}
// 拿到码后如果 showQR=true,顺便调 getMyQrcode(占位接口)
try { vm.loadQrcodeIfNeeded(); } catch(e){} try { vm.loadQrcodeIfNeeded(); } catch(e){}
}, },
fail: function(err){ fail: function(err){
@@ -970,10 +1087,61 @@ export default {
if (res && res.code === 0 && res.data && res.data.qrcode_url) { if (res && res.code === 0 && res.data && res.data.qrcode_url) {
vm.qrcodeUrl = String(res.data.qrcode_url); vm.qrcodeUrl = String(res.data.qrcode_url);
} }
// 拿到二维码后立即预生成「仅推荐码组件」分享海报
// 这样用户点"立即分享"时,storage 里已有海报,页面级 onShareAppMessage
// 同步读取后返回 imageUrl,微信就不会用全屏截图
try { vm._prepareSharePoster(); } catch(e) {}
} }
}); });
} catch (e) {} } catch (e) {}
}, },
/**
* 预生成分享海报(异步,完成后写入 _recommendShareData storage)
* 小程序端:onShareAppMessage 读取这个 storage 拿到 imageUrl
* 非小程序端:跳过,onShareClick 不依赖它
*/
_prepareSharePoster() {
const vm = this;
console.log('[推荐码分享] _prepareSharePoster 被调用,isH5Platform=', this.isH5Platform, 'code=', this.code);
// 仅小程序需要这个(其他端 onShareClick 不依赖 storage)
// #ifdef MP-WEIXIN
if (!this.code) {
console.warn('[推荐码分享] 跳过预生成:推荐码为空');
return;
}
// 安全:isH5Platform 为 true 时不进入(H5 下 canvas 不渲染,createCanvasContext 会失败)
if (this.isH5Platform) {
console.log('[推荐码分享] H5 环境跳过海报预生成');
return;
}
try {
console.log('[推荐码分享] 开始调 generateSharePoster...');
this.generateSharePoster().then(function(posterPath) {
console.log('[推荐码分享] generateSharePoster resolved, posterPath=', posterPath);
if (!posterPath) {
console.warn('[推荐码分享] 海报路径为空,跳过');
return;
}
try {
uni.setStorageSync('_recommendSharePoster', posterPath);
const data = {
title: (vm.nc && (vm.nc.shareTitle || vm.nc.title)) || '邀您入驻低空经济产业平台',
path: vm.extractMiniProgramPath(),
imageUrl: posterPath
};
uni.setStorageSync('_recommendShareData', JSON.stringify(data));
console.log('[推荐码分享] 海报预生成完成,imageUrl=', posterPath);
} catch(e) {
console.error('[推荐码分享] 写 storage 失败:', e);
}
}).catch(function(err) {
console.warn('[推荐码分享] 海报预生成失败', err);
});
} catch(e) {
console.error('[推荐码分享] _prepareSharePoster try 块异常:', e);
}
// #endif
},
onShareClick() { onShareClick() {
console.log('[推荐码分享]点击分享按钮,完整推广链接:', this.formatShareLink,'当前推荐码:',this.code); console.log('[推荐码分享]点击分享按钮,完整推广链接:', this.formatShareLink,'当前推荐码:',this.code);
@@ -1015,14 +1183,37 @@ onShareClick() {
console.log('分享链接准备,原始配置', v); console.log('分享链接准备,原始配置', v);
console.log('生成的分享完整路径:', this.extractMiniProgramPath()); console.log('生成的分享完整路径:', this.extractMiniProgramPath());
// 先用上次缓存的海报路径占位(避免用户快速二次分享时空白)
var cachedPoster = '';
try { cachedPoster = uni.getStorageSync('_recommendSharePoster') || ''; } catch(e) {}
try { try {
var shareData = { var shareData = {
title: v.shareTitle || v.title || '邀您入驻低空经济产业平台', title: v.shareTitle || v.title || '邀您入驻低空经济产业平台',
path: this.extractMiniProgramPath(), path: this.extractMiniProgramPath(),
imageUrl: v.shareImageUrl || '' imageUrl: cachedPoster || v.shareImageUrl || ''
}; };
uni.setStorageSync('_recommendShareData', JSON.stringify(shareData)); uni.setStorageSync('_recommendShareData', JSON.stringify(shareData));
} catch(e) {} } catch(e) {}
// 异步生成「仅推荐码组件」的海报图,生成后更新 storage
// 微信端:onShareAppMessage 触发时 _recommendShareData 中已是新海报
this.generateSharePoster().then(function(posterPath) {
if (!posterPath) return;
try {
// 缓存本次海报,下次分享时直接用
uni.setStorageSync('_recommendSharePoster', posterPath);
var data = {
title: v.shareTitle || v.title || '邀您入驻低空经济产业平台',
path: vm.extractMiniProgramPath(),
imageUrl: posterPath
};
uni.setStorageSync('_recommendShareData', JSON.stringify(data));
console.log('[推荐码分享]海报已生成并写入 storage', posterPath);
} catch(e) {}
}).catch(function(err) {
console.warn('[推荐码分享]海报生成失败,使用 fallback', err);
});
return; return;
// #endif // #endif
}, },
@@ -1105,14 +1296,245 @@ onShareClick() {
this.$nextTick(() => this.loadCode(false)); this.$nextTick(() => this.loadCode(false));
}, },
/** 点击未登录卡片 → 跳转登录页(和页面顶部"点击登录"一致) */ /** 点击未登录卡片 → 弹出微信手机号授权框(与会员中心登录一致) */
goLogin() { goLogin() {
uni.navigateTo({ if (this.$refs.nsLoginBox && this.$refs.nsLoginBox.open) {
url: '/pages_tool/login/login', this.$refs.nsLoginBox.open();
fail: () => { } else {
uni.redirectTo({ url: '/pages_tool/login/login' }); // 降级:ns-login 未就绪时跳转登录页
uni.navigateTo({
url: '/pages_tool/login/login',
fail: () => {
uni.redirectTo({ url: '/pages_tool/login/login' });
}
});
}
},
/**
* 生成推荐码组件的分享海报(canvas 绘制 → 临时文件路径)
* 画布内容:白底 + 标题 + 推荐码大字 + 二维码(如有) + 简短提示
* 返回 Promise<tempFilePath>
*/
generateSharePoster() {
const v = this.nc;
const code = this.code || v.placeholderCode || 'ABCD1234';
const title = v.title || '我的推荐码';
const sp = this.stylePreset;
// ★ 所有视觉参数都从 diyData.value 读取,后端可改
const palette = {
bg: v.bgColor || '#FFFFFF',
bgStart: v.bgColorStart, // 渐变起始(可选)
bgEnd: v.bgColorEnd, // 渐变结束(可选)
border: v.borderColor || '#E5E7EB',
borderRadius: Number(v.borderRadius) || 12,
padding: Number(v.padding) || 16,
titleColor: v.titleColor || '#1A1A1A',
titleFontSize: Number(v.titleFontSize) || 18,
codeColor: v.codeColor || '#DC143C',
codeFontSize: Number(v.codeFontSize) || 36,
accent: v.accentColor || '#FFD700',
text: v.textColor || '#303133',
subtitle: '#999999',
codeBlockBg: '#FFF8F0', // 推荐码区背景(米黄)
codeBlockBorder: '#FFB74D'
};
const W = 375;
const H = 480;
console.log('[推荐码分享] generateSharePoster 进入,stylePreset=', sp);
return new Promise((resolve, reject) => {
try {
const ctx = uni.createCanvasContext('sharePosterCanvas', this);
if (!ctx) return reject(new Error('createCanvasContext 失败'));
console.log('[推荐码分享] createCanvasContext 返回');
// ====================== 绘制海报 ======================
// 1. 整张背景(根据后端 bgColorStart/End 渐变,否则用 bgColor 单色)
this._drawPosterBg(ctx, W, H, palette);
// 2. 卡片(根据 stylePreset 选择布局)
this._drawPosterCard(ctx, W, H, palette, sp);
// 3. 标题(读 titleColor/titleFontSize)
ctx.setFontSize(palette.titleFontSize);
ctx.setFillStyle(palette.titleColor);
ctx.setTextAlign('left');
ctx.fillText(title, 24, 44);
// 4. 副标题(根据后端配置,可选)
const showLink = this.isFeatureOn(v.showLink);
const showQR = this.isFeatureOn(v.showQR);
ctx.setFontSize(12);
ctx.setFillStyle(palette.subtitle);
ctx.fillText('分享给好友,邀请入驻获得奖励', 24, 70);
// 4.5 如果后端开启 showLink,绘制分享链接
let urlY = 88;
if (showLink) {
try {
ctx.setFontSize(11);
ctx.setFillStyle(palette.text || '#666');
ctx.setTextAlign('left');
// 缩短显示:最多 30 字符
let linkText = (v.shareLink || this.shareLink || '').toString();
if (linkText.length > 36) linkText = linkText.slice(0, 36) + '…';
ctx.fillText('链接:' + (linkText || '(未生成)'), 24, urlY);
urlY += 16; // 后续内容往下挪
} catch (e) {}
}
// 5. 推荐码区域(读 codeColor/codeFontSize/borderRadius)
// blockY 根据 showLink 动态下移
const blockY = urlY, blockH = 110, blockX = 24, blockW = W - 48;
const codeBlockRadius = Math.min(palette.borderRadius, 16);
// 推荐码区背景
ctx.setFillStyle(palette.codeBlockBg);
this._roundRect(ctx, blockX, blockY, blockW, blockH, codeBlockRadius);
ctx.fill();
// 推荐码区边框(浅橙)
ctx.setStrokeStyle(palette.codeBlockBorder);
ctx.setLineWidth(1);
this._roundRect(ctx, blockX, blockY, blockW, blockH, codeBlockRadius);
ctx.stroke();
// 推荐码大字(居中)
ctx.setFontSize(palette.codeFontSize);
ctx.setFillStyle(palette.codeColor);
ctx.setTextAlign('center');
if (typeof ctx.setFontWeight === 'function') {
ctx.setFontWeight('bold');
} else {
try { ctx.font = 'bold ' + palette.codeFontSize + 'px sans-serif'; } catch(e) {}
}
ctx.fillText(code, W / 2, blockY + blockH / 2 + 12);
// 6. 二维码(如果有)
const qr = this.qrcodeRenderUrl;
const drawQrcodeAndExport = (qrLocalPath) => {
if (qrLocalPath) {
const qrSize = 180;
const qrX = (W - qrSize) / 2;
const qrY = blockY + blockH + 24;
// 白底框
ctx.setFillStyle('#FFFFFF');
this._roundRect(ctx, qrX - 6, qrY - 6, qrSize + 12, qrSize + 12, codeBlockRadius);
ctx.fill();
ctx.drawImage(qrLocalPath, qrX, qrY, qrSize, qrSize);
// 底部提示
ctx.setFontSize(11);
ctx.setFillStyle(palette.subtitle);
ctx.setTextAlign('center');
ctx.fillText(v.posterQrTip || '扫码进入并填写入驻信息', W / 2, qrY + qrSize + 24);
}
console.log('[推荐码分享] 准备 ctx.draw(false, cb)');
ctx.draw(false, () => {
console.log('[推荐码分享] ctx.draw callback 触发,准备 _exportCanvas');
this._exportCanvas(W, H).then(resolve).catch(reject);
});
};
if (qr) {
this._downloadImage(qr).then(drawQrcodeAndExport).catch(() => drawQrcodeAndExport(null));
} else {
drawQrcodeAndExport(null);
}
} catch (e) {
console.error('[推荐码分享] generateSharePoster try 块异常:', e);
reject(e);
} }
}); });
},
/**
* 海报背景:根据 diyData.bgColorStart/End 画渐变,否则用 bgColor 单色
*/
_drawPosterBg(ctx, W, H, palette) {
// 海报"画布外"背景:用 bgColor(浅色),不做整体渐变
try {
ctx.setFillStyle(palette.bg || '#FFFFFF');
ctx.fillRect(0, 0, W, H);
} catch (e) {
ctx.setFillStyle('#FFFFFF');
ctx.fillRect(0, 0, W, H);
}
},
/**
* 卡片绘制:画海报中部的小"卡片"(模仿组件的卡片视图)
* 根据 stylePreset 选不同视觉:
* - gradient: 卡片用 bgColorStart→bgColorEnd 渐变
* - minimal: 卡片用 bgColorStart→bgColorEnd 渐变(粉/米)
* - card: 卡片用 bgColorStart→bgColorEnd 渐变(米橙)
* - horizontal: 卡片用 bgColorStart→bgColorEnd 渐变
* 所有预设的卡片圆角读 borderRadius
*/
_drawPosterCard(ctx, W, H, palette, sp) {
// 不再画独立卡片,推荐码区本身就是"卡片"
// 这里只根据 stylePreset 在卡片右上角加 emoji 装饰
try {
ctx.setFontSize(36);
// 不同预设的装饰色 + emoji
const decos = {
gradient: { color: 'rgba(255,143,0,0.4)', emoji: '🎁' },
minimal: { color: 'rgba(255,107,107,0.35)', emoji: '🎁' },
card: { color: 'rgba(218,165,32,0.4)', emoji: '🎁' },
horizontal: { color: 'rgba(184,160,138,0.4)', emoji: '🎁' }
};
const deco = decos[sp] || decos.gradient;
ctx.setFillStyle(deco.color);
ctx.setTextAlign('right');
// 装饰位置:推荐码区右上角(blockX+W/2 区域,blockY 上方)
ctx.fillText(deco.emoji, W - 40, 78);
} catch (e) {}
},
/**
* canvas 转图片(uni.canvasToTempFilePath Promise 化)
*/
_exportCanvas(W, H) {
return new Promise((resolve, reject) => {
console.log('[推荐码分享] _exportCanvas 开始执行');
uni.canvasToTempFilePath({
canvasId: 'sharePosterCanvas',
width: W,
height: H,
destWidth: W * 2,
destHeight: H * 2,
fileType: 'png',
quality: 1,
success: (r) => {
console.log('[推荐码分享] canvasToTempFilePath 成功:', r.tempFilePath);
resolve(r.tempFilePath);
},
fail: (err) => {
console.error('[推荐码分享] canvasToTempFilePath 失败:', err);
reject(err);
}
}, this);
// 兜底超时:5s 后还没返回就 reject,避免永远卡住
setTimeout(() => {
console.warn('[推荐码分享] canvasToTempFilePath 超时');
reject(new Error('canvasToTempFilePath 超时'));
}, 5000);
});
},
/**
* 圆角矩形路径(canvas API)
*/
_roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
},
/**
* 下载图片到临时路径(Promise 化)
* 支持本地路径直接返回
*/
_downloadImage(url) {
return new Promise((resolve, reject) => {
if (!url) return reject(new Error('url 为空'));
if (!/^https?:\/\//i.test(url)) return resolve(url);
uni.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200 && res.tempFilePath) resolve(res.tempFilePath);
else reject(new Error('下载失败:' + res.statusCode));
},
fail: reject
});
});
} }
} }
}; };

View File

@@ -55,6 +55,7 @@
<uni-popup ref="videoPopup" type="center" style="background:transparent;width:100%;height:100%;"> <uni-popup ref="videoPopup" type="center" style="background:transparent;width:100%;height:100%;">
<view class="video-container" :style="popupVideoStyle"> <view class="video-container" :style="popupVideoStyle">
<video <video
:key="video_url"
ref="popupVideoRef" ref="popupVideoRef"
class="adaptive-video" class="adaptive-video"
:autoPauseIfNavigate="true" :autoPauseIfNavigate="true"
@@ -65,6 +66,9 @@
:src="video_url" :src="video_url"
:controls="true" :controls="true"
object-fit="contain" object-fit="contain"
playsinline
webkit-playsinline
x5-playsinline
@loadedmetadata="onPopupVideoLoaded" @loadedmetadata="onPopupVideoLoaded"
@error="onPopupVideoError" @error="onPopupVideoError"
></video> ></video>
@@ -144,41 +148,21 @@ export default {
// 弹窗视频容器样式 // 弹窗视频容器样式
popupVideoStyle() { popupVideoStyle() {
let style = 'position:fixed;width:100%;left:0;'; // 容器横向铺满,黑色背景填满左右边界
const defaultRatio = 9 / 16; // 视频用 object-fit:contain 保持原比例居中,左右黑边由容器黑色背景填充
let style = 'position:fixed;width:100%;height:50vh;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:0;';
let ratio = defaultRatio;
if (this.popupVideoWidth && this.popupVideoHeight) {
ratio = this.popupVideoHeight / this.popupVideoWidth;
}
const isPortrait = ratio > 1;
if (isPortrait) {
style += 'top:50%;transform:translateY(-50%);';
} else {
style += 'top:20%;';
}
// #ifdef H5
style += 'padding-top: ' + (ratio * 100) + '%;';
// #endif
// #ifdef MP-WEIXIN
style += 'padding-top: ' + (ratio * 100) + '%;';
// #endif
// #ifdef MP-HARMONY
style += 'height: ' + (this.windowWidth * ratio) + 'px;';
// #endif
return style; return style;
} }
}, },
methods: { methods: {
// 显示视频播放弹窗 // 显示视频播放弹窗
showVideo(item) { showVideo(item) {
this.video_url = item.videoUrl; let url = item.videoUrl || '';
// 处理视频URL:完整链接直接使用,否则通过 $util.img() 转换
if (url && url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
url = this.$util.img(url);
}
this.video_url = url;
this.popupVideoWidth = 0; this.popupVideoWidth = 0;
this.popupVideoHeight = 0; this.popupVideoHeight = 0;
this.$refs.videoPopup.open(); this.$refs.videoPopup.open();
@@ -625,22 +609,6 @@ export default {
left: 0; left: 0;
} }
/* #ifdef MP-WEIXIN */
.adaptive-video {
position: absolute;
top: 0;
left: 0;
}
/* #endif */
/* #ifdef MP-HARMONY */
.adaptive-video {
position: static;
display: block;
box-sizing: border-box;
}
/* #endif */
.video-nav { .video-nav {
padding: 16rpx; padding: 16rpx;
box-sizing: border-box; box-sizing: border-box;

View File

@@ -11,6 +11,9 @@
:style="videoWarpCss" :style="videoWarpCss"
controls controls
preload="metadata" preload="metadata"
playsinline
webkit-playsinline
x5-playsinline
@loadedmetadata="onVideoLoaded" @loadedmetadata="onVideoLoaded"
@click="handlerClick(videoUrl)" @click="handlerClick(videoUrl)"
></video> ></video>
@@ -106,11 +109,11 @@ export default {
} }
// #ifdef H5 // #ifdef H5
style += 'padding-top: ' + (ratio * 100) + '%;'; style += 'height:0;padding-top: ' + (ratio * 100) + '%;';
// #endif // #endif
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
style += 'padding-top: ' + (ratio * 100) + '%;'; style += 'height:0;padding-top: ' + (ratio * 100) + '%;';
// #endif // #endif
// #ifdef MP-HARMONY // #ifdef MP-HARMONY

View File

@@ -26,38 +26,17 @@
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text> <text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view> </view>
<view class="box"> <view class="box">
<!-- #ifdef MP-HARMONY --> <!-- 所有平台统一通过 formConfig.textareaRows / item.value.rows 控制显示行数:
<!-- 鸿蒙元服务渲染为原生 ArkUI TextArea,必须用 :rows 控制初始显示行数;CSS height 在原生组件上不生效 --> - H5 / 微信:原生 textarea 识别 :rows 属性,可视高度 = rows × line-height(CSS 控制 line-height);
- 鸿蒙:渲染为原生 ArkUI TextArea,CSS height 不生效,必须靠 :rows 控制。
行数和行高全部由 formConfig 动态下发,不再硬编码 176rpx / row=2。 -->
<textarea <textarea
:rows="item.value.rows || 2" :rows="item.value.rows || textareaRowsValue"
:placeholder="item.value.placeholder" :placeholder="item.value.placeholder"
placeholder-class="placeholder color-tip" placeholder-class="placeholder color-tip"
v-model="item.val" v-model="item.val"
:style="inputStyle" :style="textareaStyle"
></textarea> ></textarea>
<!-- #endif -->
<!-- #ifndef MP-HARMONY -->
<!-- #ifdef MP-WEIXIN -->
<!-- 微信小程序原生 textarea:
不用 auto-height(会随内容无限扩展遮挡后续组件),改用 textareaStyle 中
固定 height 176rpx(约 4 行)+ line-height 44rpx,可视区域比原 88rpx 翻倍,
内容多时内部滚动条 thumb 比例改善一倍,且高度可控。 -->
<textarea
:placeholder="item.value.placeholder"
placeholder-class="placeholder color-tip"
v-model="item.val"
:style="textareaStyle"
></textarea>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<textarea
:placeholder="item.value.placeholder"
placeholder-class="placeholder color-tip"
v-model="item.val"
:style="inputStyle"
></textarea>
<!-- #endif -->
<!-- #endif -->
</view> </view>
</view> </view>
</view> </view>
@@ -301,6 +280,11 @@
checkedColor: '#ff4544', checkedColor: '#ff4544',
placeholderColor: '#909399', placeholderColor: '#909399',
placeholderFontSize: 28, placeholderFontSize: 28,
// textarea 显示行数 / 行高(与 ns-newform 对齐)
// 改用 :rows + line-height 驱动高度:rows 给原生 <textarea>(H5/微信原生层/鸿蒙 ArkUI 都识别),
// line-height 用于在 textareaStyle 里算出对应的 px 高度;不再写死 176rpx。
textareaRows: 4,
textareaLineHeight: 44,
// 输入框视觉增强(与 ns-newform 对齐) // 输入框视觉增强(与 ns-newform 对齐)
inputBorderColor: '#e5e7eb', inputBorderColor: '#e5e7eb',
inputBgColor: '#f9fafb', inputBgColor: '#f9fafb',
@@ -334,6 +318,8 @@
checkedColor: pick('checkedColor', 'checked_color'), checkedColor: pick('checkedColor', 'checked_color'),
placeholderColor: pick('placeholderColor', 'placeholder_color'), placeholderColor: pick('placeholderColor', 'placeholder_color'),
placeholderFontSize: pick('placeholderFontSize', 'placeholder_font_size'), placeholderFontSize: pick('placeholderFontSize', 'placeholder_font_size'),
textareaRows: pick('textareaRows', 'textarea_rows'),
textareaLineHeight: pick('textareaLineHeight', 'textarea_line_height'),
inputBorderColor: pick('inputBorderColor', 'input_border_color'), inputBorderColor: pick('inputBorderColor', 'input_border_color'),
inputBgColor: pick('inputBgColor', 'input_bg_color'), inputBgColor: pick('inputBgColor', 'input_bg_color'),
inputFocusBorderColor: pick('inputFocusBorderColor', 'input_focus_border_color'), inputFocusBorderColor: pick('inputFocusBorderColor', 'input_focus_border_color'),
@@ -394,6 +380,8 @@
'--checked-color': cfg.checkedColor, '--checked-color': cfg.checkedColor,
'--placeholder-color': cfg.placeholderColor, '--placeholder-color': cfg.placeholderColor,
'--placeholder-font-size': this.$util.rpx(num(cfg.placeholderFontSize, 28)), '--placeholder-font-size': this.$util.rpx(num(cfg.placeholderFontSize, 28)),
'--textarea-rows': num(cfg.textareaRows, 4),
'--textarea-line-height': this.$util.rpx(num(cfg.textareaLineHeight, 44)),
// 输入框视觉增强(与 ns-newform 对齐) // 输入框视觉增强(与 ns-newform 对齐)
'--input-border-color': cfg.inputBorderColor, '--input-border-color': cfg.inputBorderColor,
'--input-bg-color': cfg.inputBgColor, '--input-bg-color': cfg.inputBgColor,
@@ -434,21 +422,37 @@
'line-height': this.$util.rpx(this.config.inputHeight) 'line-height': this.$util.rpx(this.config.inputHeight)
}); });
}, },
// textarea 专用 inline style(仅微信小程序端使用,见 template 中 #ifdef MP-WEIXIN 分支) // textarea 显示行数(来自 formConfig.textareaRows,未配置时回退到默认 4)
textareaRowsValue() {
const n = parseInt(this.config.textareaRows, 10);
return Number.isFinite(n) && n > 0 ? n : 4;
},
// textarea 行高(来自 formConfig.textareaLineHeight,用于 :rows × line-height 算出固定可视高度)
textareaLineHeightValue() {
const n = parseInt(this.config.textareaLineHeight, 10);
return Number.isFinite(n) && n > 0 ? n : 44;
},
// textarea 专用 inline style(所有平台统一使用,见 template 中 textarea 节点)
// 设计要点: // 设计要点:
// 1) 不复用 inputStyle 的 min-height / line-height:这两个值取自 inputHeight(默认 60rpx), // 1) 不复用 inputStyle 的 min-height / line-height:那两个值取自 inputHeight(默认 60rpx),
// 对 textarea 太小,会让微信小程序原生 textarea 单行过高、可视区域过小, // 对 textarea 太小,会让原生 textarea 单行过高、可视区域过小;
// 内容稍多即溢出导致原生滚动条 thumb 异常小; // 2) 不使用 auto-height:原生 textarea 上 auto-height 会让组件随内容无限扩展,
// 2) 不使用 auto-height:在微信小程序原生 textarea 上,auto-height 会让 textarea 随内容 // 遮挡后续组件,导致"看不到下面的其他组件";
// 无限扩展,遮挡后续组件,导致"看不到下面的其他组件"; // 3) 高度 = rows × lineHeight(两个值都由 formConfig 动态下发,不再写死 176rpx):
// 3) 改用固定 height 176rpx(约 4 行,line-height 44rpx),相比原 CSS height: 88rpx(2 行) // - H5 浏览器 / 微信原生层:原生 <textarea> 同时识别 :rows 和 CSS line-height,
// 可视区域翻倍,内容多时内部滚动条 thumb 比例改善一倍,且高度可控不会遮挡后续组件。 // 高度由浏览器自然算出,与 :rows 完全一致;
// - 鸿蒙 ArkUI:原生 TextArea 只识别 :rows,不识别 CSS height,所以我们在 JS 里
// 显式算 height = rows × lineHeight 反向写出,保证三端可视高度像素级一致。
textareaStyle() { textareaStyle() {
const h = this.$util.rpx(176); const lh = this.$util.rpx(this.textareaLineHeightValue);
const rows = this.textareaRowsValue;
// 加上 padding(12rpx 上下) 和 border(1rpx 上下) 的占用,避免与 :rows 算出的高度不一致时被截断
const paddings = this.$util.rpx(24) + this.$util.rpx(2);
const h = (this.$util.rpx(this.textareaLineHeightValue * (rows - 1)) + lh) + paddings;
return this.$util.objToStyle({ return this.$util.objToStyle({
'height': h, 'height': h,
'min-height': h, 'min-height': h,
'line-height': this.$util.rpx(44), 'line-height': lh,
'border-radius': this.$util.rpx(this.config.inputRadius), 'border-radius': this.$util.rpx(this.config.inputRadius),
'font-size': this.$util.rpx(this.config.labelFontSize), 'font-size': this.$util.rpx(this.config.labelFontSize),
'color': this.config.labelColor 'color': this.config.labelColor
@@ -959,6 +963,9 @@
--placeholder-font-size: 28rpx; --placeholder-font-size: 28rpx;
--checked-color: #ff4544; --checked-color: #ff4544;
--placeholder-color: #909399; --placeholder-color: #909399;
// textarea 行数 / 行高(运行时被 formWrapStyle 注入的 inline 覆盖;这里是兜底)
--textarea-rows: 4;
--textarea-line-height: 44rpx;
// 输入框视觉增强(与 ns-newform 对齐) // 输入框视觉增强(与 ns-newform 对齐)
--input-border-color: #e5e7eb; --input-border-color: #e5e7eb;
--input-bg-color: #f9fafb; --input-bg-color: #f9fafb;
@@ -1094,11 +1101,12 @@
font-size: var(--label-font-size, $font-size-base); font-size: var(--label-font-size, $font-size-base);
width: 100%; width: 100%;
color: var(--label-color, #1f2937); color: var(--label-color, #1f2937);
// 鸿蒙元服务渲染为原生 ArkUI TextArea,不识别 CSS height,由 :rows 控制行数; // 行高由 formConfig.textareaLineHeight(兜底 44rpx)控制;
// 这里给所有端都设一个最小高度基线,避免空内容时塌缩 // 高度 = rows × line-height + padding + border,所有值都从 CSS 变量读取,
min-height: 88rpx; // 真正做到了"rows 改 → 高度自动改"。
height: 88rpx; line-height: var(--textarea-line-height, 44rpx);
line-height: 44rpx; min-height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 44rpx) + 26rpx);
height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 44rpx) + 26rpx);
text-align: left; text-align: left;
border-radius: var(--input-radius, 0rpx); border-radius: var(--input-radius, 0rpx);
box-sizing: border-box; box-sizing: border-box;

View File

@@ -27,10 +27,16 @@
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text> <text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
</view> </view>
<view :class="['box', 'align-left']" :style="item._boxAlignStyle"> <view :class="['box', 'align-left']" :style="item._boxAlignStyle">
<!-- 所有平台统一通过 formConfig.textareaRows / item.value.rows 控制显示行数:
- H5 / 微信:原生 textarea 识别 :rows 属性;
- 鸿蒙:渲染为 ArkUI 原生 TextArea,CSS height 不生效,必须靠 :rows 控制。
rows 全部由 formConfig 动态下发,与 ns-form 行为保持一致。 -->
<textarea <textarea
:rows="item.value.rows || textareaRowsValue"
:placeholder="item.value.placeholder" :placeholder="item.value.placeholder"
placeholder-class="placeholder color-tip" placeholder-class="placeholder color-tip"
v-model="item.val" v-model="item.val"
:style="textareaStyle"
></textarea> ></textarea>
</view> </view>
</view> </view>
@@ -377,6 +383,10 @@ export default {
checkedColor: '#ff4544', checkedColor: '#ff4544',
placeholderColor: '#ddd', placeholderColor: '#ddd',
placeholderFontSize: 28, placeholderFontSize: 28,
// textarea 显示行数 / 行高(与 ns-form 保持一致,由 formConfig 覆盖)
// 改用 :rows + line-height 驱动高度,rows 给原生 <textarea>,line-height 在 textareaStyle 算 px 高度
textareaRows: 4,
textareaLineHeight: 44,
// 输入框视觉增强(可由后端 form_config 覆盖) // 输入框视觉增强(可由后端 form_config 覆盖)
inputBorderColor: '#e5e7eb', inputBorderColor: '#e5e7eb',
inputBgColor: '#f9fafb', inputBgColor: '#f9fafb',
@@ -452,6 +462,9 @@ export default {
'--checked-color': cfg.checkedColor, '--checked-color': cfg.checkedColor,
'--placeholder-color': cfg.placeholderColor, '--placeholder-color': cfg.placeholderColor,
'--placeholder-font-size': this.$util.rpx(num(cfg.placeholderFontSize, 28)), '--placeholder-font-size': this.$util.rpx(num(cfg.placeholderFontSize, 28)),
// textarea 行数 / 行高(CSS 兜底 + JS 算高度都用得到)
'--textarea-rows': num(cfg.textareaRows, 4),
'--textarea-line-height': this.$util.rpx(num(cfg.textareaLineHeight, 44)),
// 输入框视觉增强(边框 / 背景 / 聚焦 / label 字重) // 输入框视觉增强(边框 / 背景 / 聚焦 / label 字重)
'--input-border-color': cfg.inputBorderColor, '--input-border-color': cfg.inputBorderColor,
'--input-bg-color': cfg.inputBgColor, '--input-bg-color': cfg.inputBgColor,
@@ -497,6 +510,30 @@ export default {
'border-radius': this.$util.rpx(this.config.inputRadius) 'border-radius': this.$util.rpx(this.config.inputRadius)
}); });
}, },
// textarea 显示行数(来自 formConfig.textareaRows,未配置时回退到默认 4)
textareaRowsValue() {
const n = parseInt(this.config.textareaRows, 10);
return Number.isFinite(n) && n > 0 ? n : 4;
},
// textarea 行高(来自 formConfig.textareaLineHeight,用于 :rows × line-height 算出可视高度)
textareaLineHeightValue() {
const n = parseInt(this.config.textareaLineHeight, 10);
return Number.isFinite(n) && n > 0 ? n : 44;
},
// textarea 专用 inline style(所有平台统一使用,见 template 中 textarea 节点)
// 高度 = rows × lineHeight + padding + border,由 formConfig 动态控制,与 ns-form 同公式
textareaStyle() {
const lh = this.$util.rpx(this.textareaLineHeightValue);
const rows = this.textareaRowsValue;
const paddings = this.$util.rpx(24) + this.$util.rpx(2); // padding 12rpx 上下 + border 1rpx 上下
const h = (this.$util.rpx(this.textareaLineHeightValue * (rows - 1)) + lh) + paddings;
return this.$util.objToStyle({
'height': h,
'min-height': h,
'line-height': lh,
'border-radius': this.$util.rpx(this.config.inputRadius)
});
},
labelStyle() { labelStyle() {
const style = { const style = {
fontSize: this.$util.rpx(this.config.labelFontSize), fontSize: this.$util.rpx(this.config.labelFontSize),
@@ -997,6 +1034,9 @@ export default {
--submit-bg-color: #ff4544; --submit-bg-color: #ff4544;
--placeholder-color: #ddd; --placeholder-color: #ddd;
--placeholder-font-size: 28rpx; --placeholder-font-size: 28rpx;
// textarea 行数 / 行高(运行时被 formWrapStyle 注入的 inline 覆盖;这里是兜底)
--textarea-rows: 4;
--textarea-line-height: 44rpx;
// 输入框视觉增强(边框 / 背景 / 聚焦 / label 字重) // 输入框视觉增强(边框 / 背景 / 聚焦 / label 字重)
--input-border-color: #e5e7eb; --input-border-color: #e5e7eb;
--input-bg-color: #f9fafb; --input-bg-color: #f9fafb;
@@ -1234,8 +1274,10 @@ export default {
::v-deep textarea { ::v-deep textarea {
font-size: var(--label-font-size, 28rpx); font-size: var(--label-font-size, 28rpx);
width: 100%; width: 100%;
min-height: 120rpx; // 行高 / 高度全部由 formConfig 动态决定(CSS 兜底公式与 JS textareaStyle 一致)
line-height: 1.5; line-height: var(--textarea-line-height, 44rpx);
min-height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 44rpx) + 26rpx);
height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 44rpx) + 26rpx);
text-align: left; text-align: left;
border: 1rpx solid var(--input-border-color, #e5e7eb); border: 1rpx solid var(--input-border-color, #e5e7eb);
border-radius: var(--input-radius, 10rpx); border-radius: var(--input-radius, 10rpx);
@@ -1244,7 +1286,6 @@ export default {
color: var(--label-color, #1f2937); color: var(--label-color, #1f2937);
transition: all 0.3s ease; transition: all 0.3s ease;
box-sizing: border-box; box-sizing: border-box;
height: auto;
&:focus { &:focus {
background: var(--input-focus-bg-color, #ffffff); background: var(--input-focus-bg-color, #ffffff);
@@ -1260,8 +1301,10 @@ export default {
textarea { textarea {
font-size: var(--label-font-size, 28rpx); font-size: var(--label-font-size, 28rpx);
width: 100%; width: 100%;
min-height: 120rpx; // 行高 / 高度全部由 formConfig 动态决定(与 ::v-deep textarea 同步)
line-height: 1.5; line-height: var(--textarea-line-height, 44rpx);
min-height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 44rpx) + 26rpx);
height: calc(var(--textarea-rows, 4) * var(--textarea-line-height, 44rpx) + 26rpx);
text-align: left; text-align: left;
border: 1rpx solid var(--input-border-color, #e5e7eb); border: 1rpx solid var(--input-border-color, #e5e7eb);
border-radius: var(--input-radius, 10rpx); border-radius: var(--input-radius, 10rpx);
@@ -1270,7 +1313,6 @@ export default {
color: var(--label-color, #1f2937); color: var(--label-color, #1f2937);
transition: all 0.3s ease; transition: all 0.3s ease;
box-sizing: border-box; box-sizing: border-box;
height: auto;
&:focus { &:focus {
background: var(--input-focus-bg-color, #ffffff); background: var(--input-focus-bg-color, #ffffff);

View File

@@ -9,9 +9,13 @@
<view class="video-popup-body"> <view class="video-popup-body">
<video <video
v-if="videoUrl" v-if="videoUrl"
id="popupVideoPlayer"
:src="videoUrl" :src="videoUrl"
controls controls
autoplay autoplay
playsinline
webkit-playsinline
x5-playsinline
class="video-player" class="video-player"
></video> ></video>
<view v-else class="video-error">{{ $lang('components.ns-video-player-popup.videoUrlNotExist') }}</view> <view v-else class="video-error">{{ $lang('components.ns-video-player-popup.videoUrlNotExist') }}</view>
@@ -22,6 +26,8 @@
</template> </template>
<script> <script>
import { playVideoInContext } from '@/common/js/video-helper.js';
export default { export default {
name: 'ns-video-player-popup', name: 'ns-video-player-popup',
props: { props: {
@@ -76,6 +82,10 @@ export default {
* @param {Object} e - 弹窗状态变化事件 * @param {Object} e - 弹窗状态变化事件
*/ */
onVideoPopupChange(e) { onVideoPopupChange(e) {
// H5 端弹窗显示后触发播放:避免 autoplay 被浏览器拦截 / 弹窗动画未完成被 pause() 打断
if (e && e.show) {
playVideoInContext('popupVideoPlayer', this);
}
this.$emit('popup-change', e); this.$emit('popup-change', e);
} }
} }

View File

@@ -10,6 +10,4 @@ export const lang = {
// Modal dialog // Modal dialog
tip: 'Tip', tip: 'Tip',
paramError: 'Parameter error', paramError: 'Parameter error',
// Back to home
backHome: 'Back to Home',
} }

View File

@@ -9,6 +9,4 @@ export const lang = {
// 弹窗 // 弹窗
tip: '提示', tip: '提示',
paramError: '参数错误', paramError: '参数错误',
// 返回主页
backHome: '返回主页',
} }

View File

@@ -2152,7 +2152,9 @@
{ {
"path": "form/formdata", "path": "form/formdata",
"style": { "style": {
// #ifdef APP-PLUS // APP 端原本就是 custom;微信小程序 / 华为快应用 / 鸿蒙元服务
// 的原生 navbar 不支持自定义左侧 Home 按钮,需要切到 custom 自绘
// #ifdef APP-PLUS || MP
"navigationStyle": "custom", "navigationStyle": "custom",
// #endif // #endif
"navigationBarTitleText": "", "navigationBarTitleText": "",

View File

@@ -59,7 +59,8 @@
<!-- 商品视频 --> <!-- 商品视频 -->
<view class="goods-video" :class="{ show: switchMedia == 'video' }"> <view class="goods-video" :class="{ show: switchMedia == 'video' }">
<video id="goodsVideo" :src="$util.img(goodsSkuDetail.video_url)" <video id="goodsVideo" :src="$util.img(goodsSkuDetail.video_url)"
:poster="$util.img(goodsSkuDetail.sku_image, { size: 'big' })" objectFit="cover"></video> :poster="$util.img(goodsSkuDetail.sku_image, { size: 'big' })" objectFit="cover"
playsinline webkit-playsinline x5-playsinline></video>
</view> </view>
<!-- 切换视频、图片 --> <!-- 切换视频、图片 -->

View File

@@ -1,16 +1,34 @@
<template> <template>
<view :style="themeColor"> <view :style="themeColor">
<!-- 返回主页按钮:左上角固定,始终可见 --> <!--
<view class="home-btn" @click="goHome"> 自绘导航栏(仅在 navigationStyle:custom 的端渲染:APP / 微信小程序 / 华为快应用 / 鸿蒙元服务)
<text class="home-btn-text">{{ $lang('form.formdata.backHome') }}</text> - pages.json 已为这几端开启 navigationStyle:custom,系统不再提供原生 navbar
- 左侧:hasBackStack 为 true 时显示返回箭头(点击 navigateBack),showHomeBtn=true 时再叠加 Home 胶囊
- 中央:表单标题(form_name)
- 整体高度 = 状态栏高度 + 44px 内容区,由 customNavbarStyle 动态算
H5 端保留系统原生 navbar,不渲染此块
-->
<!-- #ifndef H5 -->
<view class="form-home-navbar" :style="customNavbarStyle">
<view class="form-home-navbar__left">
<view v-if="hasBackStack" class="form-home-navbar__back" @tap="goBack" hover-class="form-home-navbar__btn--hover">
<text class="form-home-navbar__back-icon">‹</text>
</view>
<view v-if="showHomeBtn" class="form-home-capsule" @tap="goHome" hover-class="form-home-capsule--hover">
<text class="iconfont icon-shouye1 form-home-capsule__icon"></text>
</view>
</view>
<view class="form-home-navbar__title">{{ navbarTitle }}</view>
<view class="form-home-navbar__placeholder" />
</view> </view>
<!-- #endif -->
<block v-if="detail"> <block v-if="detail">
<view class="page"> <view class="page">
<view class="system-form-wrap"> <view class="system-form-wrap">
<!-- 复用 diy-form:banner / tip / 表单字段 / 提交按钮 全部由其内部渲染,样式(bannerPaddingX/tipColor/submitBgColor 等)由 form_config 控制。 <!-- 复用 diy-form:banner / tip / 表单字段 / 提交按钮 全部由其内部渲染,样式(bannerPaddingX/tipColor/submitBgColor 等)由 form_config 控制。
:onSubmit 接管提交逻辑(带 uniacid + showModal);跨 H5 / MP 一致,不依赖 $listeners。 :onSubmit 接管提交逻辑(带 uniacid + showModal);跨 H5 / MP 一致,不依赖 $listeners。
注意:create 重度依赖本组件 this(id/uniacid/memberInfo/$api 等),必须在 created 阶段 bind this, 注意:create 重度依赖本组件 this(id/uniacid/memberInfo/$api 等),必须在 created 阶段 bind this,
否则 diy-form 通过 this.onSubmit(formData) 调用时 this 会漂移到子组件实例 --> 否则 diy-form 通过 this.onSubmit(formData) 调用时 this 会漂移到子组件实例 -->
<diy-form ref="form" :value="diyValue" dataMode="raw" :onSubmit="handleSubmit" /> <diy-form ref="form" :value="diyValue" dataMode="raw" :onSubmit="handleSubmit" />
</view> </view>
</view> </view>
@@ -27,13 +45,9 @@
<script> <script>
import diyForm from '@/components-diy/diy-form.vue';
import scroll from '@/common/js/scroll-view.js'; import scroll from '@/common/js/scroll-view.js';
export default { export default {
components: {
diyForm
},
mixins: [scroll], mixins: [scroll],
data() { data() {
return { return {
@@ -45,9 +59,17 @@ export default {
uniacid: 0, uniacid: 0,
isFromMpWebView: false, // 是否是从微信小程序的WebView跳转过来的 isFromMpWebView: false, // 是否是从微信小程序的WebView跳转过来的
hideNavbar: false, // 查询参数中支持隐藏Navbar hideNavbar: false, // 查询参数中支持隐藏Navbar
showHomeBtn: false, // 查询参数 show_home=1 时,在系统标题栏左侧显示 Home 按钮
urlParams: {}, // 通用 URL 透传参数收集 urlParams: {}, // 通用 URL 透传参数收集
handleSubmit: null, // create 的 this 绑定版本,作为 :onSubmit 传给 diy-form(见 created) handleSubmit: null, // create 的 this 绑定版本,作为 :onSubmit 传给 diy-form(见 created)
// #ifndef H5
navbarHeight: 44, // 自绘 navbar 内容区高度(不含状态栏),跨端兜底 44px
statusBarHeight: 20, // 系统状态栏高度,自绘 navbar 时需要预留顶部空隙
navbarTitle: '', // 自绘 navbar 中央显示的标题
hasBackStack: false, // 自绘 navbar 左上角是否显示返回箭头:页面栈 > 1 时显示
// #endif
// #ifdef H5 // #ifdef H5
formTitle: '', // 查询参数传入的Title formTitle: '', // 查询参数传入的Title
enableSetTitle: false, // 查询参数传入的是否可以强制设置Title enableSetTitle: false, // 查询参数传入的是否可以强制设置Title
@@ -62,6 +84,22 @@ export default {
this.handleSubmit = this.create.bind(this); this.handleSubmit = this.create.bind(this);
}, },
computed: { computed: {
// #ifndef H5
// 自绘 navbar 的高度样式
// - 内容区高度 = navbarHeight(兜底 44px)
// - 整体高度 = 状态栏高度 + 内容区高度
// - padding-top 把内容区下移到状态栏之下,避免被刘海/水滴遮挡
customNavbarStyle() {
try {
const sys = uni.getSystemInfoSync();
const sb = sys.statusBarHeight || 0;
const h = this.navbarHeight || 44;
return `height: ${sb + h}px; padding-top: ${sb}px;`;
} catch (e) {
return 'height: 64px; padding-top: 20px;';
}
},
// #endif
// 构造 diy-form raw 模式期望的 value 形态: // 构造 diy-form raw 模式期望的 value 形态:
// - jsonData:透传给 ns-newform(已是 ns-newform 期望的 [{ controller, value, val }]) // - jsonData:透传给 ns-newform(已是 ns-newform 期望的 [{ controller, value, val }])
// - formConfig:后端返回 camelCase(如 formPaddingX / formBgColor / labelFontSize), // - formConfig:后端返回 camelCase(如 formPaddingX / formBgColor / labelFontSize),
@@ -102,11 +140,36 @@ export default {
this.uniacid = options.uniacid this.uniacid = options.uniacid
this.id = options.id || 0 this.id = options.id || 0
this.isFromMpWebView = options.from === 'mp-webview'; this.isFromMpWebView = options.from === 'mp-webview';
this.hideNavbar = Boolean(options?.hide_navbar); this.hideNavbar = this.$util.coerceBool(options?.hide_navbar);
// show_home=1 时在系统标题栏左侧显示 Home 按钮,点击回到首页 INDEX_PAGE_URL
// 解析规则统一走 this.$util.coerceBool:支持 '1' / 'true' / 1 / true 四种写法
// 仅在 navigationStyle:custom 端生效(APP / 微信小程序 / 华为快应用 / 鸿蒙元服务);
// H5 保留系统原生 navbar,不支持 show_home,参数被忽略(这是预期行为)
// 注:H5 嵌入微信小程序 webview(from=mp-webview)的场景也属于 H5 范畴,
// 此场景下也没有 Home 入口——后续如需支持,需要单独处理(导航靠宿主小程序接管)
this.showHomeBtn = this.$util.coerceBool(options?.show_home);
// #ifndef H5
// 自绘 navbar 需要用到状态栏高度 / navbar 内容区高度(跨端兜底)
try {
const sys = uni.getSystemInfoSync();
if (sys.statusBarHeight) this.statusBarHeight = sys.statusBarHeight;
// uniapp 在不同端 menuButton / navigationBar 高度略有差异,给个安全兜底即可
if (typeof sys.navbarHeight === 'number' && sys.navbarHeight > 0) {
this.navbarHeight = sys.navbarHeight;
}
} catch (e) { /* keep defaults */ }
// 自绘 navbar 是否显示返回箭头:getCurrentPages().length > 1 即页面有栈
try {
const pages = getCurrentPages ? getCurrentPages() : [];
this.hasBackStack = Array.isArray(pages) && pages.length > 1;
} catch (e) { /* keep default */ }
// #endif
// 通用 URL 参数收集:除业务保留字段外,其余全部透传给后端 // 通用 URL 参数收集:除业务保留字段外,其余全部透传给后端
// 后端会做卫生过滤并广播事件,供 recommend 等插件消费 // 后端会做卫生过滤并广播事件,供 recommend 等插件消费
const reservedKeys = ['uniacid', 'id', 'scene', 'from', 'hide_navbar', 'title', 'enable_set_title']; const reservedKeys = ['uniacid', 'id', 'scene', 'from', 'hide_navbar', 'show_home', 'title', 'enable_set_title'];
this.urlParams = {}; this.urlParams = {};
for (let key in options) { for (let key in options) {
if (reservedKeys.indexOf(key) !== -1) continue; if (reservedKeys.indexOf(key) !== -1) continue;
@@ -149,6 +212,20 @@ export default {
this.updateNavbarTitle(); this.updateNavbarTitle();
}, },
methods: { methods: {
// #ifndef H5
// 返回上一页;用 navigateBack,栈深度>1 时由 hasBackStack 控制按钮可见性
goBack() {
uni.navigateBack({ delta: 1 });
},
// #endif
// 返回首页;
// INDEX_PAGE_URL 是 TabBar 根页面,不能用裸 uni.redirectTo(会报"can not redirectTo a tabBar page")
// 必须用 this.$util.redirectTo,内部对 TabBar 页面自动走 uni.switchTab
// (鸿蒙元服务下还会用 reLaunch 兜底,见 common/js/util.js)
// H5 端保留 show_home / goHome(虽然模板里不渲染自绘 navbar,但保留方法调用不报错)
goHome() {
this.$util.redirectTo(this.$util.INDEX_PAGE_URL, {}, 'redirectTo');
},
getData() { getData() {
this.$api.sendRequest({ this.$api.sendRequest({
url: '/form/api/form/info', url: '/form/api/form/info',
@@ -176,19 +253,15 @@ export default {
formName = this.formTitle; formName = this.formTitle;
} }
// #endif // #endif
// H5 端:系统原生 navbar —— uni.setNavigationBarTitle 改原生标题
// #ifdef H5
uni.setNavigationBarTitle({ title: formName }); uni.setNavigationBarTitle({ title: formName });
}, // #endif
// 返回主页(用 reLaunch 避免返回后还能 navigateBack 回到已提交的表单页) // 非 H5 端:custom 模式下没有原生 navbar,直接写 data.navbarTitle,
goHome() { // 由模板里的 .form-home-navbar__title 显示
this.$util.redirectTo(this.$util.INDEX_PAGE_URL, {}, 'reLaunch'); // #ifndef H5
}, this.navbarTitle = formName;
// 清空表单数据:通过 diy-form 的 ref 拿到 ns-newform 实例,调用 setFormData 重置 // #endif
resetForm() {
try {
if (this.$refs.form && this.$refs.form.$refs.nsNewform && this.$refs.form.$refs.nsNewform.setFormData) {
this.$refs.form.$refs.nsNewform.setFormData();
}
} catch(e) {}
}, },
// 由 diy-form 内置提交按钮触发:diy-form handleSubmit 已先做 verify(), // 由 diy-form 内置提交按钮触发:diy-form handleSubmit 已先做 verify(),
// 仅在通过时才通过 :onSubmit 函数 prop 调用本方法(跨 H5 / MP 一致,不走 $emit / $listeners)。 // 仅在通过时才通过 :onSubmit 函数 prop 调用本方法(跨 H5 / MP 一致,不走 $emit / $listeners)。
@@ -209,18 +282,15 @@ export default {
member_id: this.memberInfo ? this.memberInfo.id : 0 member_id: this.memberInfo ? this.memberInfo.id : 0
}, },
success: res => { success: res => {
if (res.code == 0) { if (res.code == 0) {
this.complete = true this.complete = true
// 提交成功后清空表单 uni.showModal({
this.resetForm() title: this.$lang('form.formdata.tip'),
this.isRepeat = false content: this.$lang('form.formdata.submitSuccess'),
uni.showModal({ showCancel: false,
title: this.$lang('form.formdata.tip'), success: () => { }
content: this.$lang('form.formdata.submitSuccess'), })
showCancel: false, } else {
success: () => { }
})
} else {
this.isRepeat = false this.isRepeat = false
this.$util.showToast({ this.$util.showToast({
title: res.message title: res.message
@@ -237,6 +307,114 @@ export default {
} }
</script> </script>
<style lang="scss" scoped>
/*
* 自绘 navbar(非 H5 端:APP / 微信小程序 / 华为快应用 / 鸿蒙元服务)
* - pages.json 已为这几端开启 navigationStyle:custom,系统不再提供原生 navbar
* - 整体高度由 inline style 注入:statusBarHeight + 44px
* - 左侧 Home 胶囊 + 中央标题 + 右侧占位,标题过长自动省略
*/
/* #ifndef H5 */
/* --- 自绘 navbar 容器 --- */
.form-home-navbar {
position: relative;
width: 100%;
box-sizing: border-box;
background: #ffffff;
/* 高度由 inline style 通过 customNavbarStyle 注入:statusBarHeight + 44px */
/* padding-top 把内容区下移到状态栏之下,避免被刘海/水滴遮挡 */
/* 浮在表单内容之上,避免被 diy-form 等组件覆盖 */
z-index: 100;
/* 让内容靠 flex 自动落到 padding-top 之下的区域 */
display: flex;
flex-direction: row;
align-items: center;
}
.form-home-navbar__left {
/* 不再用 absolute,跟着 navbar 的 padding-top 下移 */
flex: none;
/* 宽度自适应:返回按钮 56rpx + Home 胶囊 56rpx + 间距 12rpx + 左 padding 16rpx = 最多 ~140rpx */
min-width: 88rpx;
max-width: 200rpx;
display: flex;
align-items: center;
justify-content: flex-start;
padding-left: 16rpx;
box-sizing: border-box;
gap: 12rpx;
}
.form-home-navbar__back {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
}
.form-home-navbar__back-icon {
font-size: 48rpx;
line-height: 1;
color: #000;
font-weight: 300;
}
.form-home-navbar__btn--hover {
background: rgba(0, 0, 0, 0.06);
}
.form-home-navbar__title {
flex: 1;
height: 44px;
line-height: 44px;
text-align: center;
font-size: 32rpx;
color: #000;
font-weight: 500;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
box-sizing: border-box;
}
.form-home-navbar__placeholder {
/* 与 __left 对称,保持标题视觉居中 */
flex: none;
min-width: 88rpx;
max-width: 200rpx;
}
/* --- Home 胶囊主体 --- */
.form-home-capsule {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
}
.form-home-capsule--hover {
background: rgba(0, 0, 0, 0.06);
}
/*
* Home 图标 —— 使用项目统一的 iconfont icon-shouye1(unicode \e02d)
* 与 pages_goods/detail.vue 的首页按钮共用同一个图标,跨端视觉一致
*/
.form-home-capsule__icon {
font-size: 32rpx;
line-height: 1;
color: #000;
display: block;
}
/* #endif */
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
page { page {
background: #fff; background: #fff;
@@ -254,35 +432,6 @@ page {
border-radius: 0; border-radius: 0;
} }
// 左上角返回主页按钮
.home-btn {
position: fixed;
left: 30rpx;
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-LARK
// 小程序端需避开右上角胶囊按钮,下移到状态栏 + 胶囊高度之下
top: calc(var(--status-bar-height, 20px) + 10rpx);
// #endif
// #ifdef H5
top: 20rpx;
// #endif
z-index: 999;
padding: 12rpx 28rpx;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 40rpx;
display: flex;
align-items: center;
.home-btn-text {
color: #ffffff;
font-size: 26rpx;
line-height: 1;
}
&:active {
opacity: 0.8;
}
}
::v-deep .mescroll-totop { ::v-deep .mescroll-totop {
/* #ifdef H5 */ /* #ifdef H5 */
right: 27rpx !important; right: 27rpx !important;