Compare commits
1 Commits
temp2797
...
yuanlaide3
| Author | SHA1 | Date | |
|---|---|---|---|
| a137f3533c |
78
.gitignore
vendored
78
.gitignore
vendored
@@ -7,80 +7,8 @@
|
||||
.codebuddy
|
||||
.vscode
|
||||
.trae
|
||||
# 本地调试配置 (含 uniacid / 域名, 每人不同, 不应入仓)
|
||||
# 注意: 如果 .local.config.js 已经被之前的 commit 入仓,
|
||||
# 需要手动 `git rm --cached .local.config.js` 并用 filter-branch
|
||||
# 清理历史 (仅限团队协商后操作)
|
||||
.local.config.js
|
||||
|
||||
# 鸿蒙元服务构建产物 (被 .gitignore 的只是产物, HAR 元数据需要 git 管理)
|
||||
# 重要: ascf_build 根目录的元数据 (oh-package.json5 / Index.ets / build-profile.json5 / ...)
|
||||
# 必须被 git 管理, 否则无法追踪 "module.name 被清空" 等回归。
|
||||
# 这些元数据通过下方的 ! 取反规则显式纳入 git。
|
||||
# 鸿蒙元服务构建产物
|
||||
stub-app-projects/mp-harmony/ascf/ascf_src
|
||||
stub-app-projects/mp-harmony/entry/src/main/resources/rawfile
|
||||
|
||||
# ascf_build/<pkg>/ 是 HAR 包定义
|
||||
# 区分两类内容:
|
||||
# 1. 必须入 git (元数据 / 构建配置 / HAR 入口):
|
||||
# - oh-package.json5, build-profile.json5, hvigorfile.ts
|
||||
# - obfuscation-rules.txt, src/main/module.json5
|
||||
# - src/main/resources/base/element/string.json (HAR i18n)
|
||||
# 2. 每次构建覆盖 (不应入 git):
|
||||
# - src/main/resources/rawfile/** : 鸿蒙端从 ascf_src 同步的编译产物
|
||||
# - Index.ets : 空壳文件, 无内容
|
||||
# - build/, oh_modules/, .preview/, .cxx/, .test/, node_modules/
|
||||
|
||||
# rawfile/ 整体 ignore (鸿蒙端每次 Build 重新生成)
|
||||
stub-app-projects/mp-harmony/ascf/ascf_build/**/rawfile/**
|
||||
|
||||
# Index.ets 是空壳 (每个包都是 0 字节), 不入 git
|
||||
stub-app-projects/mp-harmony/ascf/ascf_build/**/Index.ets
|
||||
|
||||
# 其他产物目录
|
||||
stub-app-projects/mp-harmony/ascf/ascf_build/**/build
|
||||
stub-app-projects/mp-harmony/ascf/ascf_build/**/oh_modules
|
||||
stub-app-projects/mp-harmony/ascf/ascf_build/**/.preview
|
||||
stub-app-projects/mp-harmony/ascf/ascf_build/**/.cxx
|
||||
stub-app-projects/mp-harmony/ascf/ascf_build/**/.test
|
||||
stub-app-projects/mp-harmony/ascf/ascf_build/**/node_modules
|
||||
|
||||
# entry 下的产物
|
||||
stub-app-projects/mp-harmony/entry/build
|
||||
stub-app-projects/mp-harmony/entry/oh_modules
|
||||
stub-app-projects/mp-harmony/entry/.preview
|
||||
stub-app-projects/mp-harmony/entry/.cxx
|
||||
stub-app-projects/mp-harmony/entry/.test
|
||||
stub-app-projects/mp-harmony/entry/node_modules
|
||||
# 鸿蒙工程顶层产物 (.hvigor / hvigor / .idea 已在根 .gitignore 范围或更精准的鸿蒙子 .gitignore)
|
||||
stub-app-projects/mp-harmony/oh_modules
|
||||
stub-app-projects/mp-harmony/.hvigor
|
||||
stub-app-projects/mp-harmony/.idea
|
||||
# 鸿蒙 hvigor 自身依赖
|
||||
stub-app-projects/mp-harmony/hvigor
|
||||
# Trae/IDE 临时文件
|
||||
.hvigor/outputs
|
||||
.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)
|
||||
# 仓库有 origin (github.com/Zajd/jsysq-uniapp) 是公开仓库, 签名材料必须 gitignore
|
||||
stub-app-projects/mp-harmony/**/cert-profile
|
||||
stub-app-projects/mp-harmony/**/.p12
|
||||
stub-app-projects/mp-harmony/**/.p7b
|
||||
stub-app-projects/mp-harmony/**/.cer
|
||||
stub-app-projects/mp-harmony/**/.csr
|
||||
stub-app-projects/mp-harmony/**/material
|
||||
stub-app-projects/mp-harmony/ascf/ascf_build
|
||||
stub-app-projects/mp-harmony/entry/src/main/resources
|
||||
@@ -59,15 +59,10 @@ const localDevConfig = ({
|
||||
domain: 'http://192.168.197.1:8050/',
|
||||
// domain: 'http://localhost:8050/',
|
||||
},
|
||||
'192.168.8.118-1': { // 测试平台
|
||||
uniacid: 1,
|
||||
domain: 'http://192.168.8.118:8050/',
|
||||
// domain: 'http://localhost:8050/',
|
||||
},
|
||||
'local-11': { // 测试平台
|
||||
uniacid: 11,
|
||||
domain: 'http://localhost:8050/',
|
||||
},
|
||||
},
|
||||
'100': {
|
||||
uniacid: 100,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
@@ -88,10 +83,6 @@ const localDevConfig = ({
|
||||
uniacid: 206,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
'5g-165': { // 鸡肉调理品, 优惠卷显示不全,及逻辑错误
|
||||
uniacid: 165,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
'47': { // 修改弹窗
|
||||
uniacid: 47,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
@@ -136,50 +127,32 @@ const localDevConfig = ({
|
||||
uniacid: 2821,
|
||||
domain: 'https://xcx.aigc-quickapp.com/',
|
||||
},
|
||||
'18': { // 测试平台
|
||||
uniacid: 18,
|
||||
'54': { // 测试平台
|
||||
uniacid: 54,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
'2': { // 测试平台
|
||||
uniacid: 2,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
'48': { // 测试平台
|
||||
uniacid: 48,
|
||||
'54': { // 测试平台
|
||||
uniacid: 54,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
'test-12': { // 户外鞋,测试优惠券
|
||||
uniacid: 12,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
'248': { // 大宗商品贸易,支持自定义字体大小、颜色
|
||||
uniacid: 248,
|
||||
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-97']; // 大宗商品贸易,支持自定义字体大小、颜色
|
||||
// })['5g-50']; // 燃气安全产品机器人,购物车无反应
|
||||
// })['2821']; // 离型纸硅油纸
|
||||
// })['2482']; // 低空经济
|
||||
})['54']; // 低空经济
|
||||
// })['48']; // 离型纸硅油纸
|
||||
// })['2485']; // 验证电子表单前后端数据一致性
|
||||
// })['5g-11']; // 数码
|
||||
// })['5g-213']; // 选择要使用的环境配置
|
||||
// })['2881']; // 鸡肉调理品, 优惠卷显示不全,及逻辑错误
|
||||
// })['2724']; // 生物菌肥
|
||||
// })['5g-213']; // 选择要使用的环境配置
|
||||
// })['2811']; // 选择要使用的环境配置
|
||||
// })['local-11']; // 选择要使用的环境配置
|
||||
})['2797']; // 本地
|
||||
// })['local-1']; // 本地
|
||||
// })['local-2']; // 本地
|
||||
// })['local-11']; // 本地
|
||||
// })['1-test']; // test
|
||||
// })['test-12']; // test
|
||||
// })['18-test']; // test
|
||||
export default localDevConfig;
|
||||
33
App.vue
33
App.vue
@@ -271,56 +271,27 @@
|
||||
// 隐私协议弹窗: 启动期关键资源 (i18n/theme/remote-config) 就绪后触发
|
||||
// 通过 PrivacyManager → uni.$emit('privacy:show') 通知当前页面的 privacy-popup 组件
|
||||
// 只弹一次: 同意后写 storage,后续不再弹
|
||||
// 异步查询平台隐私状态(微信/快应用), H5 同步返回
|
||||
// 异步查询平台隐私状态(微信/鸿蒙), H5 同步返回
|
||||
// H5 直访特定 URL(子串匹配)时跳过本次弹窗,不写 storage,下次正常访问其他页面还会弹
|
||||
// urlFallback: 首次启动 onLaunch 时 getCurrentPages 还没页面, 用 onLaunch options 兜底
|
||||
// readyGuard: 双保险 — 若调用方没传, PrivacyManager 内部从 _externalReadyGuard 取
|
||||
// (= ConfigExternal.getPrivacyReadyGuard(privacyConfig.waitChannels))
|
||||
// 二次 await 仍保留: race 后还有可能某个通道没真完成, 二次 await 兜底
|
||||
//
|
||||
// 鸿蒙元服务(双轨制):
|
||||
// 已接入平台隐私托管, 鸿蒙系统自动弹出系统级隐私弹窗.
|
||||
// 应用层不调 PrivacyManager.show() (会和系统弹窗叠加被驳回).
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙: 走平台隐私托管, markSkipped 写 storage
|
||||
try {
|
||||
await Promise.all([_i18nDone, _themeFirstApply, _themeOverride, _remoteConfig])
|
||||
PrivacyManager.markSkipped()
|
||||
} catch (e) { console.warn('[privacy] MP-HARMONY markSkipped fail', e); }
|
||||
// #endif
|
||||
// #ifndef MP-HARMONY
|
||||
try {
|
||||
await PrivacyManager.show({
|
||||
urlFallback: _buildPrivacyUrlFallback(options),
|
||||
readyGuard: Promise.all([_i18nDone, _themeFirstApply, _themeOverride, _remoteConfig]),
|
||||
})
|
||||
} catch (e) { console.warn('[privacy] show fail', e); }
|
||||
// #endif
|
||||
|
||||
// splash 必须在隐私弹窗决策后再 hide (否则 splash 关掉瞬间弹窗还没渲染, 视觉闪一下)
|
||||
// mode='fast': 隐私弹窗即将出现, splash 用 80ms 短动画, 让 modal 更快接管视觉
|
||||
// 避免 splash 和弹窗同时显示造成的"双遮罩"感, 减少约 400ms 启动时间
|
||||
// 只有 remote-only 模式才用了 splash (其他场景 src 模式不需要等远端)
|
||||
//
|
||||
// 关键 (鸿蒙 + 其他平台): 这里只发"软隐藏"信号, splash 实际关闭要等
|
||||
// splashState._softHide() 看到 _hasMounted && _pageReady 才真关。
|
||||
// _pageReady 由首页 diy-group 渲染完时调 notifyPageReady() 触发。
|
||||
// 这样保证 splash **一直显示**, 直到首页 diy 内容真的渲染好,
|
||||
// 避免 splash 关闭后到 diy 内容出现之间出现白屏。
|
||||
//
|
||||
// 鸿蒙端 (mode='strict'):
|
||||
// 鸿蒙系统弹窗是用户视觉锚点, 应用层 splash 必须**真等到**diy 渲染好
|
||||
// 才能关, 不走 MOUNT_GRACE_MS/PAGE_READY_GRACE_MS 软隐藏兜底
|
||||
// (软隐藏兜底 1+2=3s 经常不够用, 会导致 splash 提前 hide → 白屏)
|
||||
// strict 模式只受 DEFAULT_TIMEOUT=12s 总超时兜底, 实际由 notifyPageReady() 触发
|
||||
// #ifndef H5
|
||||
const _needSplash = typeof Lang !== 'undefined' && Lang.isRemoteOnly && Lang.isRemoteOnly();
|
||||
if (_needSplash) {
|
||||
// 所有平台都用 strict 模式: 不走 1+2s 软隐藏兜底
|
||||
// splash 真等到 notifyPageReady (diy 渲染好) 或 notifyDiyLoadError (diy 错误) 才关
|
||||
// DEFAULT_TIMEOUT=12s 总超时仍然兜底
|
||||
// 避免 splash 提前关导致白屏
|
||||
splashState.hide({ reason: 'boot-complete', mode: 'strict' });
|
||||
splashState.hide({ reason: 'boot-complete', mode: 'fast' });
|
||||
}
|
||||
// #endif
|
||||
|
||||
|
||||
152
common/js/diy.js
152
common/js/diy.js
@@ -1,7 +1,6 @@
|
||||
import WxMap from 'common/js/map-wx-jssdk.js';
|
||||
import Config from '@/common/js/config.js';
|
||||
import util from '@/common/js/util.js';
|
||||
import { classifyHttpError, httpErrorKey } from '@/common/js/http-error.js';
|
||||
|
||||
let systemInfo = util.getDeviceInfo();
|
||||
|
||||
@@ -52,15 +51,10 @@ export default {
|
||||
lastDiyInfoTime: 0, // 上次成功加载的时间戳
|
||||
lastDiyInfoLang: '', // 上次加载时的语言,语言切换时强制刷新
|
||||
diyInfoTtl: 3 * 60 * 1000, // 3分钟内不重复请求
|
||||
|
||||
// diy 数据网络加载失败时的页面内错误提示(比 showModal 在各端更可靠)
|
||||
diyLoadError: false,
|
||||
diyLoadErrorMsg: '',
|
||||
diyLoadErrorUrl: '', // 出错的网络请求地址
|
||||
|
||||
|
||||
//启动广告
|
||||
adv: {},
|
||||
|
||||
adv:{},
|
||||
|
||||
};
|
||||
},
|
||||
onLoad(option) {
|
||||
@@ -123,10 +117,10 @@ export default {
|
||||
// 关闭定位
|
||||
this.enterDefaultStore();
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
onShow() {
|
||||
|
||||
|
||||
this.init();
|
||||
},
|
||||
onHide() {
|
||||
@@ -211,12 +205,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
play() {
|
||||
play(){
|
||||
},
|
||||
async init() {
|
||||
|
||||
// 定位信息过期后,重新获取定位
|
||||
if (this.mapConfig.wap_is_open == 1 && this.locationStorage && this.locationStorage.is_expired) {
|
||||
if(this.mapConfig.wap_is_open == 1 && this.locationStorage && this.locationStorage.is_expired) {
|
||||
this.$util.getLocation({
|
||||
fail: (res) => {
|
||||
// 拒绝定位,进入默认总店
|
||||
@@ -232,7 +226,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[DIY][init] calling getDiyInfo, storeToken=', !!this.storeToken);
|
||||
await this.getDiyInfo();
|
||||
//获取启动广告
|
||||
await this.getDiyAdv();
|
||||
@@ -290,7 +283,7 @@ export default {
|
||||
}, 50);
|
||||
// #endif
|
||||
},
|
||||
async getDiyAdv() {
|
||||
async getDiyAdv(){
|
||||
//启动广告
|
||||
let res = await this.$api.sendRequest({
|
||||
url: '/api/diyview/getstartadv',
|
||||
@@ -299,13 +292,13 @@ export default {
|
||||
});
|
||||
this.adv = res.value
|
||||
// 弹框形式,首次弹出 1,每次弹出 0
|
||||
if (this.adv.advshow == 1) {
|
||||
if(this.adv.advshow == 1){
|
||||
setTimeout(() => {
|
||||
if (res.value.advtype == 1) {
|
||||
var popwindow_count = uni.getStorageSync(this.id + this.name + '_popwindow_count');
|
||||
if ((this.$refs.uniPopupWindow && popwindow_count == '') || (
|
||||
this.$refs.uniPopupWindow && popwindow_count == 1)) {
|
||||
|
||||
|
||||
this.$refs.uniPopupWindow.open();
|
||||
uni.setStorageSync(this.id + this.name + '_popwindow_count', 1);
|
||||
}
|
||||
@@ -315,75 +308,25 @@ export default {
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
},
|
||||
// 无法从网络获取 diy 数据时:在页面内渲染错误横幅(各端可靠、一定可见)
|
||||
// 同时通过 uni.$emit 通知 splash-state 关闭启动屏 (否则 strict 模式永远不关)
|
||||
// @param {Object|string} [err] 网络异常对象 {errMsg, statusCode} / 服务端消息字符串
|
||||
// @param {string} [url] 出错的请求地址(完整 URL),用于横幅展示
|
||||
// @param {string} [errorType] 错误类型: 'network' | 'no-response' | 'business'
|
||||
_showDiyLoadError(err, url, errorType) {
|
||||
this.diyLoadErrorMsg = this._buildDiyErrorMsg(err);
|
||||
this.diyLoadErrorUrl = url || '';
|
||||
this.diyLoadError = true;
|
||||
// 通知 splash: diy 加载失败, 启动屏应该让位给错误横幅
|
||||
// 用 uni.$emit 而非直接 import splash-state, 避免循环依赖
|
||||
// reason 拼成 'diy-{errorType}-error' 方便业务方区分 (splash:diy-error 事件)
|
||||
const reason = errorType ? `diy-${errorType}-error` : 'diy-load-error'
|
||||
try { uni.$emit && uni.$emit('diy:load-error', { reason, errorType, err, url }); } catch (e) { }
|
||||
},
|
||||
// 根据错误信息组织更详细的提示文案
|
||||
// 说明:remote-only 模式下 $lang 可能返回空串,故统一用 `|| 中文兜底` 保证非空
|
||||
_buildDiyErrorMsg(err) {
|
||||
const info = classifyHttpError(err);
|
||||
|
||||
// 服务端明确返回的业务消息(字符串):直接透传
|
||||
if (info.type === 'business') return info.raw;
|
||||
|
||||
const key = httpErrorKey(info.type);
|
||||
let reason = this.$lang(key);
|
||||
|
||||
// HTTP 状态码类错误:把 statusCode 拼到 reason 后面
|
||||
if (info.statusCode) reason += '(' + info.statusCode + ')';
|
||||
|
||||
// empty / unknown 不走模板,直接返回通用网络异常
|
||||
if (info.type === 'empty' || info.type === 'unknown') return reason;
|
||||
|
||||
const tpl = this.$lang('diy.loadFailTpl', { reason });
|
||||
return tpl;
|
||||
},
|
||||
// 错误横幅「重试」入口
|
||||
retryDiyInfo() {
|
||||
this.diyLoadError = false;
|
||||
this.diyLoadErrorMsg = '';
|
||||
this.diyLoadErrorUrl = '';
|
||||
this.getDiyInfo(true);
|
||||
|
||||
},
|
||||
async getDiyInfo(force = false) {
|
||||
// 节流:正在请求 / N 秒内已加载过 / 语言未变 -> 跳过
|
||||
const now = Date.now();
|
||||
const currentLang = uni.getStorageSync("lang") || '';
|
||||
console.log('[DIY][getDiyInfo] start', 'force=', force, 'id=', this.id, 'name=', this.name, 'lang=', currentLang, 'loading=', this.diyInfoLoading, 'lastAt=', this.lastDiyInfoTime, 'lastLang=', this.lastDiyInfoLang);
|
||||
if (!force) {
|
||||
if (this.diyInfoLoading) {
|
||||
console.log('[DIY][getDiyInfo] skip: already loading');
|
||||
return;
|
||||
}
|
||||
if (this.diyInfoLoading) return;
|
||||
if (this.lastDiyInfoTime && (now - this.lastDiyInfoTime) < this.diyInfoTtl
|
||||
&& this.lastDiyInfoLang === currentLang) {
|
||||
console.log('[DIY][getDiyInfo] skip: throttled by ttl');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.diyInfoLoading = true;
|
||||
let res;
|
||||
// 出错时用于展示的完整请求地址(Request URL),与 http.js 拼接/归一化方式保持一致
|
||||
const diyInfoUrl = '/api/diyview/info';
|
||||
const diyInfoFullUrl = ((Config.baseUrl || '') + diyInfoUrl).replace(/(?<!:)\/+/g, '/');
|
||||
try {
|
||||
res = await this.$api.sendRequest({
|
||||
url: diyInfoUrl,
|
||||
url: '/api/diyview/info',
|
||||
data: {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
@@ -392,30 +335,19 @@ export default {
|
||||
async: false
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[DIY][getDiyInfo] request failed', e);
|
||||
console.error('getDiyInfo request failed', e);
|
||||
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
|
||||
this.$util.showToast({
|
||||
title: this.$lang('diy.networkError')
|
||||
});
|
||||
this.diyInfoLoading = false;
|
||||
// 无法从网络获取数据时,页面内显示错误横幅并提供重试(带详细原因 + 请求地址)
|
||||
// errorType='network': 网络异常 (uni.request catch)
|
||||
this._showDiyLoadError(e, diyInfoFullUrl, 'network');
|
||||
return;
|
||||
}
|
||||
this.diyInfoLoading = false;
|
||||
console.log('[DIY][getDiyInfo] got response', 'code=', res && res.code, 'hasData=', !!(res && res.data), 'valueType=', res && res.data && typeof res.data.value, 'valueLen=', res && res.data && (typeof res.data.value === 'string' ? res.data.value.length : (res.data.value ? res.data.value.length : null)));
|
||||
|
||||
if (!res || res.code != 0 || !res.data) {
|
||||
console.warn('[DIY][getDiyInfo] invalid response', JSON.stringify(res));
|
||||
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
|
||||
|
||||
// 完全没有拿到网络响应,视为无法从网络获取数据,页面内显示错误横幅
|
||||
if (!res) {
|
||||
// errorType='no-response': 完全没有 res 对象
|
||||
this._showDiyLoadError(null, diyInfoFullUrl, 'no-response');
|
||||
// 保持原有数据结构,仅清空 value,避免鸿蒙下重新布局
|
||||
this.diyData = { ...(this.diyData || {}), value: [] };
|
||||
return;
|
||||
}
|
||||
|
||||
if (res && res.code == -3) {
|
||||
this.$util.showToast({
|
||||
title: res.message
|
||||
@@ -425,10 +357,9 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// 拿到响应但业务失败 / 无数据(如接口 404 返回 200+错误码、code!=0)
|
||||
// 同样视为无法获取到 diy 数据,页面内显示错误横幅
|
||||
// errorType='business': 业务失败 (code!=0 / 消息提示)
|
||||
this._showDiyLoadError(res && res.message, diyInfoFullUrl, 'business');
|
||||
this.$util.showToast({
|
||||
title: this.$lang('diy.noDiyData')
|
||||
});
|
||||
// 保持原有数据结构,仅清空 value,避免鸿蒙下重新布局
|
||||
this.diyData = { ...(this.diyData || {}), value: [] };
|
||||
return;
|
||||
@@ -436,20 +367,10 @@ export default {
|
||||
|
||||
this.lastDiyInfoTime = now;
|
||||
this.lastDiyInfoLang = currentLang;
|
||||
// 成功加载后清除错误提示
|
||||
this.diyLoadError = false;
|
||||
this.diyLoadErrorMsg = '';
|
||||
this.diyLoadErrorUrl = '';
|
||||
|
||||
let diyDataValue = res.data;
|
||||
if (diyDataValue.value) {
|
||||
try {
|
||||
this.diyData = JSON.parse(diyDataValue.value);
|
||||
} catch (parseErr) {
|
||||
console.error('[DIY][getDiyInfo] JSON.parse(value) failed', parseErr, 'value preview=', String(diyDataValue.value).slice(0, 200));
|
||||
throw parseErr;
|
||||
}
|
||||
console.log('[DIY][getDiyInfo] parsed diyData', 'globalKeys=', this.diyData && this.diyData.global ? Object.keys(this.diyData.global) : null, 'valueCount=', this.diyData && this.diyData.value && this.diyData.value.length, 'topIndexValue before=', this.topIndexValue);
|
||||
this.diyData = JSON.parse(diyDataValue.value);
|
||||
|
||||
// 导航栏标题要根据语言环境切换
|
||||
// 优先使用 title_map 多语言 map,没有则回退到 title / en_title
|
||||
@@ -458,22 +379,22 @@ export default {
|
||||
|
||||
this.mpCollect = this.diyData.global.mpCollect;
|
||||
this.setPublicShare();
|
||||
/* if (this.diyData.global.popWindow && this.diyData.global.popWindow.imageUrl) {
|
||||
// 弹框形式,首次弹出 1,每次弹出 0
|
||||
setTimeout(() => {
|
||||
if (this.diyData.global.popWindow.count == 1) {
|
||||
var popwindow_count = uni.getStorageSync(this.id + this.name + '_popwindow_count');
|
||||
if ((this.$refs.uniPopupWindow && popwindow_count == '') || (
|
||||
this.$refs.uniPopupWindow && popwindow_count == 1)) {
|
||||
this.$refs.uniPopupWindow.open();
|
||||
uni.setStorageSync(this.id + this.name + '_popwindow_count', 1);
|
||||
}
|
||||
} else if (this.diyData.global.popWindow.count == 0) {
|
||||
/* if (this.diyData.global.popWindow && this.diyData.global.popWindow.imageUrl) {
|
||||
// 弹框形式,首次弹出 1,每次弹出 0
|
||||
setTimeout(() => {
|
||||
if (this.diyData.global.popWindow.count == 1) {
|
||||
var popwindow_count = uni.getStorageSync(this.id + this.name + '_popwindow_count');
|
||||
if ((this.$refs.uniPopupWindow && popwindow_count == '') || (
|
||||
this.$refs.uniPopupWindow && popwindow_count == 1)) {
|
||||
this.$refs.uniPopupWindow.open();
|
||||
uni.setStorageSync(this.id + this.name + '_popwindow_count', 0);
|
||||
uni.setStorageSync(this.id + this.name + '_popwindow_count', 1);
|
||||
}
|
||||
}, 500);
|
||||
}*/
|
||||
} else if (this.diyData.global.popWindow.count == 0) {
|
||||
this.$refs.uniPopupWindow.open();
|
||||
uni.setStorageSync(this.id + this.name + '_popwindow_count', 0);
|
||||
}
|
||||
}, 500);
|
||||
}*/
|
||||
|
||||
// 修改diy数据结构排序
|
||||
let searchIndex = -1;
|
||||
@@ -537,9 +458,6 @@ export default {
|
||||
this.openBottomNav = this.diyData.global.openBottomNav;
|
||||
}
|
||||
this.isShowCopyRight = true;
|
||||
console.log('[DIY][getDiyInfo] done', 'topIndexValue after=', this.topIndexValue && this.topIndexValue.componentName, 'valueCount=', this.diyData.value.length, 'openBottomNav=', this.openBottomNav, 'isShowCopyRight=', this.isShowCopyRight);
|
||||
} else {
|
||||
console.warn('[DIY][getDiyInfo] no value in res.data', JSON.stringify(diyDataValue).slice(0, 200));
|
||||
}
|
||||
},
|
||||
closePopupWindow() {
|
||||
|
||||
@@ -44,7 +44,7 @@ export class EventSafety {
|
||||
// 安全的方法
|
||||
matches: (selector) => EventSafety.safeMatches(target, selector),
|
||||
// 数据集
|
||||
dataset: target?.dataset || {}
|
||||
dataset: target.dataset || {}
|
||||
}
|
||||
|
||||
return safeTarget
|
||||
|
||||
@@ -80,126 +80,113 @@ export default {
|
||||
methods: {
|
||||
// 处理商品详情数据
|
||||
async handleGoodsSkuData() {
|
||||
// 鸿蒙兼容: 外层包 try/catch, 任何同步抛错都不会导致 page 整体空白
|
||||
try {
|
||||
if (this.goodsSkuDetail.config) {
|
||||
this.navbarData.navBarSwitch = this.goodsSkuDetail.config.nav_bar_switch;
|
||||
// this.navbarData.title = this.goodsSkuDetail.goods_name.length > 9 ? this.goodsSkuDetail.goods_name.substr(0, 9) + "..." : this.goodsSkuDetail.goods_name;
|
||||
//设置标题
|
||||
// this.$langConfig.title(this.navbarData.title);
|
||||
if (this.goodsSkuDetail.config) {
|
||||
this.navbarData.navBarSwitch = this.goodsSkuDetail.config.nav_bar_switch;
|
||||
}
|
||||
|
||||
this.whetherCollection = this.goodsSkuDetail.is_collect; // 用户关注商品状态
|
||||
|
||||
this.modifyGoodsInfo();
|
||||
|
||||
// 初始化商品详情视图数据
|
||||
if (this.$refs.goodsDetailView) this.$refs.goodsDetailView.init({
|
||||
sku_id: this.skuId,
|
||||
goods_id: this.goodsSkuDetail.goods_id,
|
||||
preview: this.preview,
|
||||
source_member: this.source_member,
|
||||
posterParams: this.posterParams,
|
||||
posterApi: this.posterApi,
|
||||
shareUrl: this.shareUrl,
|
||||
goodsRoute: this.goodsRoute,
|
||||
isVirtual: this.goodsSkuDetail.is_virtual,
|
||||
deliveryType: this.goodsSkuDetail.express_type,
|
||||
whetherCollection: this.goodsSkuDetail.is_collect,
|
||||
evaluateConfig: this.goodsSkuDetail.evaluate_config,
|
||||
evaluateList: this.goodsSkuDetail.evaluate_list,
|
||||
evaluateCount: this.goodsSkuDetail.evaluate_count
|
||||
});
|
||||
|
||||
//媒体
|
||||
if (this.goodsSkuDetail.video_url) this.switchMedia = "video";
|
||||
|
||||
if (!Array.isArray(this.goodsSkuDetail.sku_images)) {
|
||||
if (this.goodsSkuDetail.sku_images) this.goodsSkuDetail.sku_images = this.goodsSkuDetail.sku_images.split(",");
|
||||
else this.goodsSkuDetail.sku_images = [];
|
||||
}
|
||||
|
||||
// 多规格时合并主图
|
||||
if (this.goodsSkuDetail.goods_spec_format && this.goodsSkuDetail.goods_image) {
|
||||
|
||||
if (!Array.isArray(this.goodsSkuDetail.goods_image)) this.goodsSkuDetail.goods_image = this.goodsSkuDetail.goods_image.split(",");
|
||||
|
||||
this.goodsSkuDetail.sku_images = this.goodsSkuDetail.goods_image.concat(this.goodsSkuDetail.sku_images);
|
||||
}
|
||||
|
||||
let maxHeight = '';
|
||||
let systemInfo = uni.getSystemInfoSync();
|
||||
this.goodsSkuDetail.goods_image_list.forEach((item, index) => {
|
||||
if (typeof item.pic_spec == "string")
|
||||
item.pic_spec = item.pic_spec.split('*');
|
||||
|
||||
let ratio = item.pic_spec[0] / systemInfo.windowWidth;
|
||||
item.pic_spec[0] = item.pic_spec[0] / ratio;
|
||||
item.pic_spec[1] = item.pic_spec[1] / ratio;
|
||||
|
||||
if (!maxHeight || maxHeight > item.pic_spec[1]) {
|
||||
maxHeight = item.pic_spec[1];
|
||||
}
|
||||
});
|
||||
this.goodsSkuDetail.swiperHeight = maxHeight + 'px';
|
||||
|
||||
this.whetherCollection = this.goodsSkuDetail.is_collect; // 用户关注商品状态
|
||||
this.goodsSkuDetail.unit = this.goodsSkuDetail.unit || this.$lang('unit.piece');
|
||||
|
||||
this.modifyGoodsInfo();
|
||||
// 当前商品SKU规格
|
||||
if (this.goodsSkuDetail.sku_spec_format) this.goodsSkuDetail.sku_spec_format = JSON.parse(this.goodsSkuDetail.sku_spec_format);
|
||||
|
||||
// 初始化商品详情视图数据
|
||||
try {
|
||||
if (this.$refs && this.$refs.goodsDetailView) {
|
||||
this.$refs.goodsDetailView.init({
|
||||
sku_id: this.skuId,
|
||||
goods_id: this.goodsSkuDetail.goods_id,
|
||||
preview: this.preview,
|
||||
source_member: this.source_member,
|
||||
posterParams: this.posterParams,
|
||||
posterApi: this.posterApi,
|
||||
shareUrl: this.shareUrl,
|
||||
goodsRoute: this.goodsRoute,
|
||||
isVirtual: this.goodsSkuDetail.is_virtual,
|
||||
deliveryType: this.goodsSkuDetail.express_type,
|
||||
whetherCollection: this.goodsSkuDetail.is_collect,
|
||||
evaluateConfig: this.goodsSkuDetail.evaluate_config,
|
||||
evaluateList: this.goodsSkuDetail.evaluate_list,
|
||||
evaluateCount: this.goodsSkuDetail.evaluate_count
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('goodsDetailView.init failed:', e);
|
||||
}
|
||||
|
||||
//媒体
|
||||
if (this.goodsSkuDetail.video_url) this.switchMedia = "video";
|
||||
|
||||
if (!Array.isArray(this.goodsSkuDetail.sku_images)) {
|
||||
if (this.goodsSkuDetail.sku_images) this.goodsSkuDetail.sku_images = this.goodsSkuDetail.sku_images.split(",");
|
||||
else this.goodsSkuDetail.sku_images = [];
|
||||
}
|
||||
|
||||
// 多规格时合并主图
|
||||
if (this.goodsSkuDetail.goods_spec_format && this.goodsSkuDetail.goods_image) {
|
||||
|
||||
if (!Array.isArray(this.goodsSkuDetail.goods_image)) this.goodsSkuDetail.goods_image = this.goodsSkuDetail.goods_image.split(",");
|
||||
|
||||
this.goodsSkuDetail.sku_images = this.goodsSkuDetail.goods_image.concat(this.goodsSkuDetail.sku_images);
|
||||
}
|
||||
|
||||
let maxHeight = '';
|
||||
let systemInfo = uni.getSystemInfoSync();
|
||||
this.goodsSkuDetail.goods_image_list.forEach((item, index) => {
|
||||
if (typeof item.pic_spec == "string")
|
||||
item.pic_spec = item.pic_spec.split('*');
|
||||
|
||||
let ratio = item.pic_spec[0] / systemInfo.windowWidth;
|
||||
item.pic_spec[0] = item.pic_spec[0] / ratio;
|
||||
item.pic_spec[1] = item.pic_spec[1] / ratio;
|
||||
|
||||
if (!maxHeight || maxHeight > item.pic_spec[1]) {
|
||||
maxHeight = item.pic_spec[1];
|
||||
}
|
||||
});
|
||||
this.goodsSkuDetail.swiperHeight = maxHeight + 'px';
|
||||
|
||||
this.goodsSkuDetail.unit = this.goodsSkuDetail.unit || this.$lang('unit.piece');
|
||||
|
||||
// 当前商品SKU规格
|
||||
if (this.goodsSkuDetail.sku_spec_format) this.goodsSkuDetail.sku_spec_format = JSON.parse(this.goodsSkuDetail.sku_spec_format);
|
||||
|
||||
// 商品属性
|
||||
if (this.goodsSkuDetail.goods_attr_format) {
|
||||
let goods_attr_format = JSON.parse(this.goodsSkuDetail.goods_attr_format);
|
||||
this.goodsSkuDetail.goods_attr_format = this.$util.unique(goods_attr_format, "attr_id");
|
||||
for (var i = 0; i < this.goodsSkuDetail.goods_attr_format.length; i++) {
|
||||
for (var j = 0; j < goods_attr_format.length; j++) {
|
||||
if (this.goodsSkuDetail.goods_attr_format[i].attr_id == goods_attr_format[j].attr_id && this.goodsSkuDetail.goods_attr_format[i].attr_value_id != goods_attr_format[j].attr_value_id) {
|
||||
this.goodsSkuDetail.goods_attr_format[i].attr_value_name += "、" + goods_attr_format[j].attr_value_name;
|
||||
}
|
||||
// 商品属性
|
||||
if (this.goodsSkuDetail.goods_attr_format) {
|
||||
let goods_attr_format = JSON.parse(this.goodsSkuDetail.goods_attr_format);
|
||||
this.goodsSkuDetail.goods_attr_format = this.$util.unique(goods_attr_format, "attr_id");
|
||||
for (var i = 0; i < this.goodsSkuDetail.goods_attr_format.length; i++) {
|
||||
for (var j = 0; j < goods_attr_format.length; j++) {
|
||||
if (this.goodsSkuDetail.goods_attr_format[i].attr_id == goods_attr_format[j].attr_id && this.goodsSkuDetail.goods_attr_format[i].attr_value_id != goods_attr_format[j].attr_value_id) {
|
||||
this.goodsSkuDetail.goods_attr_format[i].attr_value_name += "、" + goods_attr_format[j].attr_value_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 商品SKU格式
|
||||
if (this.goodsSkuDetail.goods_spec_format) this.goodsSkuDetail.goods_spec_format = JSON.parse(this.goodsSkuDetail.goods_spec_format);
|
||||
// 商品SKU格式
|
||||
if (this.goodsSkuDetail.goods_spec_format) this.goodsSkuDetail.goods_spec_format = JSON.parse(this.goodsSkuDetail.goods_spec_format);
|
||||
|
||||
// 商品详情
|
||||
if (this.goodsSkuDetail.goods_content) this.goodsSkuDetail.goods_content = (this.goodsSkuDetail.goods_content);
|
||||
// if (this.goodsSkuDetail.goods_content) this.goodsSkuDetail.goods_content = htmlParser(this.goodsSkuDetail.goods_content);
|
||||
// 商品详情
|
||||
if (this.goodsSkuDetail.goods_content) this.goodsSkuDetail.goods_content = (this.goodsSkuDetail.goods_content);
|
||||
// if (this.goodsSkuDetail.goods_content) this.goodsSkuDetail.goods_content = htmlParser(this.goodsSkuDetail.goods_content);
|
||||
|
||||
//商品服务
|
||||
if (this.goodsSkuDetail.goods_service) {
|
||||
for (let i in this.goodsSkuDetail.goods_service) {
|
||||
this.goodsSkuDetail.goods_service[i]['icon'] = this.goodsSkuDetail.goods_service[i]['icon'] ? JSON.parse(this.goodsSkuDetail.goods_service[i]['icon']) : '';
|
||||
}
|
||||
//商品服务
|
||||
if (this.goodsSkuDetail.goods_service) {
|
||||
for (let i in this.goodsSkuDetail.goods_service) {
|
||||
this.goodsSkuDetail.goods_service[i]['icon'] = this.goodsSkuDetail.goods_service[i]['icon'] ? JSON.parse(this.goodsSkuDetail.goods_service[i]['icon']) : '';
|
||||
}
|
||||
}
|
||||
|
||||
this.contactData = {
|
||||
title: this.goodsSkuDetail.sku_name,
|
||||
path: this.shareUrl,
|
||||
img: this.$util.img(this.goodsSkuDetail.sku_image, {
|
||||
size: 'big'
|
||||
})
|
||||
};
|
||||
try {
|
||||
if (this.$refs && this.$refs.goodsPromotion) {
|
||||
this.$refs.goodsPromotion.refresh(this.goodsSkuDetail.goods_promotion);
|
||||
}
|
||||
} catch (e) { console.error('goodsPromotion refresh failed:', e); }
|
||||
this.contactData = {
|
||||
title: this.goodsSkuDetail.sku_name,
|
||||
path: this.shareUrl,
|
||||
img: this.$util.img(this.goodsSkuDetail.sku_image, {
|
||||
size: 'big'
|
||||
})
|
||||
};
|
||||
if (this.$refs.goodsPromotion) this.$refs.goodsPromotion.refresh(this.goodsSkuDetail.goods_promotion);
|
||||
|
||||
// 性能优化: 商品表单(留言表单)仅在商品详情打开"立即留言"弹窗时才需要,
|
||||
// 不再阻塞首次渲染。改为在后台并发请求, 用户真正打开弹窗时数据已就绪。
|
||||
try {
|
||||
this._goodsFormPromise = this.getGoodsForm();
|
||||
} catch (e) {
|
||||
console.error('getGoodsForm kicked off failed:', e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('handleGoodsSkuData failed:', e);
|
||||
if (this.goodsRoute != '/pages_goods/detail') this.setPublicShare();
|
||||
|
||||
// this.getBarrageData();
|
||||
if (this.addonIsExist.form) {
|
||||
await this.getGoodsForm();
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -340,35 +327,13 @@ export default {
|
||||
* 给 detail.vue 的 diy-form 直接消费。所有分支都做兜底初始化,避免下游拿到 undefined。
|
||||
*/
|
||||
async getGoodsForm() {
|
||||
// 防御: sku 切换/数据未到时 goods_id 可能为 0/空, 此时直接走空数据分支,
|
||||
// 避免后端拿到非法 goods_id 导致 4xx/5xx 后还把脏数据塞给前端。
|
||||
if (!this.goodsSkuDetail || !this.goodsSkuDetail.goods_id) {
|
||||
const emptyFn = () => {
|
||||
this.$set(this.goodsSkuDetail, 'goods_form', []);
|
||||
this.$set(this.goodsSkuDetail, 'goods_form_config', {});
|
||||
this.$set(this.goodsSkuDetail, 'goods_form_name', '');
|
||||
this.$set(this.goodsSkuDetail, 'goods_form_tip', '');
|
||||
this.$set(this.goodsSkuDetail, 'goods_form_banner', '');
|
||||
this.$set(this.goodsSkuDetail, 'goods_form_show_banner', false);
|
||||
this.$set(this.goodsSkuDetail, 'goods_form_show_tip', false);
|
||||
};
|
||||
emptyFn();
|
||||
return;
|
||||
}
|
||||
let res;
|
||||
try {
|
||||
res = await this.$api.sendRequest({
|
||||
url: "/form/api/form/goodsform",
|
||||
data: {
|
||||
goods_id: this.goodsSkuDetail.goods_id,
|
||||
format: 'object'
|
||||
},
|
||||
async: false
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('getGoodsForm request failed', e);
|
||||
res = null;
|
||||
}
|
||||
const res = await this.$api.sendRequest({
|
||||
url: "/form/api/form/goodsform",
|
||||
data: {
|
||||
goods_id: this.goodsSkuDetail.goods_id
|
||||
},
|
||||
async: false
|
||||
});
|
||||
const empty = () => {
|
||||
this.$set(this.goodsSkuDetail, 'goods_form', []);
|
||||
this.$set(this.goodsSkuDetail, 'goods_form_config', {});
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
/**
|
||||
* 通用网络/接口错误分类器
|
||||
*
|
||||
* 适用场景:
|
||||
* - uniapp 各端统一错误分类(diy.js / 商品 / 订单 / 自定义页面等)
|
||||
* - 任意传入 {statusCode, errMsg, message} 形式错误对象的位置
|
||||
*
|
||||
* 设计要点:
|
||||
* 1) 与 Vue / $lang 解耦:不调用任何 i18n 函数,只返回分类结果 + 原始错误字段
|
||||
* 调用方拿到 type 后,自行决定使用 $lang('http.timeout') / $lang('http.serverError')
|
||||
* 或其它命名空间的文案,完全保持各模块原有的 key 命名习惯
|
||||
* 2) 通用网络/接口错误统一映射到 common.http.* 命名空间:
|
||||
* - http.networkError: 通用网络异常(兜底)
|
||||
* - http.timeout: 请求超时
|
||||
* - http.serverError: 服务器错误(5xx)
|
||||
* - http.notFound: 接口不存在(404)
|
||||
* - http.auth: 无访问权限(401/403)
|
||||
* - http.unknown: 其他非 2xx
|
||||
* 3) 兼容多种错误对象形态:
|
||||
* - uniapp request: { statusCode, errMsg, data }
|
||||
* - axios: { response: { status }, message }
|
||||
* - fetch / 原生: { message, name }
|
||||
*
|
||||
* @param {Object|string} err 错误对象或后端业务消息字符串
|
||||
* @returns {{ type: string, statusCode?: number, errMsg?: string, raw?: any }}
|
||||
* - type 取值:
|
||||
* 'business' - 后端返回的业务消息字符串(已透传到 raw)
|
||||
* 'empty' - 错误对象为空
|
||||
* 'notFound' - HTTP 404
|
||||
* 'server' - HTTP 5xx
|
||||
* 'auth' - HTTP 401 / 403
|
||||
* 'request' - 其它非 2xx 状态码
|
||||
* 'timeout' - errMsg 含 timeout
|
||||
* 'network' - errMsg 含 connect / network / fail
|
||||
* 'unknown' - 无法分类
|
||||
* - statusCode: 若有则原样返回
|
||||
* - errMsg: 归一后的原始消息
|
||||
* - raw: 原始输入,方便调用方进一步处理
|
||||
*/
|
||||
export function classifyHttpError(err) {
|
||||
// 1) 无错误对象
|
||||
if (!err) {
|
||||
return { type: 'empty', raw: err };
|
||||
}
|
||||
|
||||
// 2) 后端直接返回的业务消息(字符串)
|
||||
if (typeof err === 'string') {
|
||||
return { type: 'business', raw: err };
|
||||
}
|
||||
|
||||
// 3) 字段归一(兼容 uni / axios / 原生 Error)
|
||||
const statusCode = err.statusCode
|
||||
?? err.response?.status
|
||||
?? err.status;
|
||||
const errMsg = err.errMsg || err.message || '';
|
||||
|
||||
if (statusCode) {
|
||||
if (statusCode === 404) return { type: 'notFound', statusCode, errMsg, raw: err };
|
||||
if (statusCode >= 500) return { type: 'server', statusCode, errMsg, raw: err };
|
||||
if (statusCode === 401 || statusCode === 403) return { type: 'auth', statusCode, errMsg, raw: err };
|
||||
return { type: 'request', statusCode, errMsg, raw: err };
|
||||
}
|
||||
|
||||
if (/timeout/i.test(errMsg)) {
|
||||
return { type: 'timeout', errMsg, raw: err };
|
||||
}
|
||||
if (/(connect|network|fail)/i.test(errMsg)) {
|
||||
return { type: 'network', errMsg, raw: err };
|
||||
}
|
||||
|
||||
return { type: 'unknown', errMsg, raw: err };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 classifyHttpError 的结果,映射到 common.http.* 命名空间的 i18n key
|
||||
*
|
||||
* 通用网络/接口错误统一走 http.*,调用方通过 $lang(key) 解析文案。
|
||||
* 原 diy.errTimeout 这类 key 已合并到 http.*,保持项目国际化更专业。
|
||||
*
|
||||
* @param {string} type classifyHttpError 返回的 type
|
||||
* @returns {string|null} 对应的 http.* key;type='business' 时返回 null(透传 raw)
|
||||
*/
|
||||
export function httpErrorKey(type) {
|
||||
switch (type) {
|
||||
case 'empty': return 'http.networkError';
|
||||
case 'business': return null; // 透传,不用 key
|
||||
case 'notFound': return 'http.notFound';
|
||||
case 'server': return 'http.serverError';
|
||||
case 'auth': return 'http.auth';
|
||||
case 'request': return 'http.unknown';
|
||||
case 'timeout': return 'http.timeout';
|
||||
case 'network': return 'http.networkError';
|
||||
case 'unknown':
|
||||
default: return 'http.networkError';
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
classifyHttpError,
|
||||
httpErrorKey,
|
||||
};
|
||||
@@ -44,15 +44,6 @@ function _sleep(ms) {
|
||||
}
|
||||
|
||||
function _makeRequest(url, method, data, header, dataType, responseType, timeout) {
|
||||
const reqId = 'req_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
|
||||
const reqStart = Date.now();
|
||||
console.log('[HTTP][REQUEST]', reqId, method, url, JSON.stringify({
|
||||
data: data,
|
||||
header: header,
|
||||
dataType: dataType,
|
||||
responseType: responseType,
|
||||
timeout: timeout
|
||||
}));
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: url,
|
||||
@@ -63,7 +54,6 @@ function _makeRequest(url, method, data, header, dataType, responseType, timeout
|
||||
responseType: responseType,
|
||||
timeout: timeout,
|
||||
success: (res) => {
|
||||
console.log('[HTTP][RESPONSE]', reqId, method, url, 'statusCode=', res.statusCode, 'cost=', (Date.now() - reqStart) + 'ms', JSON.stringify(res.data));
|
||||
if (res.statusCode && [200, 500].indexOf(res.statusCode) == -1) {
|
||||
reject({ errMsg: `request:fail statusCode=${res.statusCode}`, statusCode: res.statusCode });
|
||||
return;
|
||||
@@ -83,7 +73,6 @@ function _makeRequest(url, method, data, header, dataType, responseType, timeout
|
||||
resolve(res.data);
|
||||
},
|
||||
fail: (res) => {
|
||||
console.error('[HTTP][FAIL]', reqId, method, url, 'cost=', (Date.now() - reqStart) + 'ms', JSON.stringify(res));
|
||||
reject(res);
|
||||
}
|
||||
});
|
||||
@@ -98,20 +87,15 @@ async function _requestWithRetry(params, url, method, data, header, dataType, re
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
if (attempt > 0) {
|
||||
console.log('[HTTP][RETRY]', method, url, 'attempt=', attempt, 'maxRetries=', maxRetries);
|
||||
}
|
||||
const result = await _makeRequest(url, method, data, header, dataType, responseType, timeout);
|
||||
return result;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
lastStatusCode = err && err.statusCode;
|
||||
console.warn('[HTTP][RETRY-FAIL]', method, url, 'attempt=', attempt, 'errMsg=', err && err.errMsg, 'statusCode=', lastStatusCode);
|
||||
if (attempt >= maxRetries || !_shouldRetry(err.errMsg)) {
|
||||
break;
|
||||
}
|
||||
const delay = RETRY_DELAY_BASE * Math.pow(2, attempt);
|
||||
console.log('[HTTP][RETRY-WAIT]', method, url, 'delay=', delay + 'ms');
|
||||
await _sleep(delay);
|
||||
}
|
||||
}
|
||||
@@ -223,7 +207,6 @@ export default {
|
||||
}
|
||||
},
|
||||
uploadBase64(params) {
|
||||
console.log('[HTTP][REQUEST] uploadBase64', Config.baseUrl + '/api/upload/headimgBase64');
|
||||
uni.request({
|
||||
url: Config.baseUrl + '/api/upload/headimgBase64',
|
||||
method: 'POST',
|
||||
@@ -238,17 +221,14 @@ export default {
|
||||
dataType: 'json',
|
||||
responseType: 'text',
|
||||
success: res => {
|
||||
console.log('[HTTP][RESPONSE] uploadBase64', 'statusCode=', res.statusCode, JSON.stringify(res.data));
|
||||
typeof params.success == 'function' && params.success(res.data);
|
||||
},
|
||||
fail: () => {
|
||||
console.error('[HTTP][FAIL] uploadBase64', JSON.stringify(res));
|
||||
typeof params.fail == 'function' && params.fail(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
pullImage(params) {
|
||||
console.log('[HTTP][REQUEST] pullImage', Config.baseUrl + '/api/upload/headimgPull', 'path=', params.path);
|
||||
uni.request({
|
||||
url: Config.baseUrl + '/api/upload/headimgPull',
|
||||
method: 'POST',
|
||||
@@ -263,17 +243,14 @@ export default {
|
||||
dataType: 'json',
|
||||
responseType: 'text',
|
||||
success: res => {
|
||||
console.log('[HTTP][RESPONSE] pullImage', 'statusCode=', res.statusCode, JSON.stringify(res.data));
|
||||
typeof params.success == 'function' && params.success(res.data);
|
||||
},
|
||||
fail: () => {
|
||||
console.error('[HTTP][FAIL] pullImage', JSON.stringify(res));
|
||||
typeof params.fail == 'function' && params.fail(res);
|
||||
}
|
||||
});
|
||||
},
|
||||
upload(params) {
|
||||
console.log('[HTTP][REQUEST] upload', Config.baseUrl + params.url, 'name=', params.name || 'file');
|
||||
uni.uploadFile({
|
||||
url: Config.baseUrl + params.url,
|
||||
filePath: params.filePath,
|
||||
@@ -287,11 +264,9 @@ export default {
|
||||
'content-type': 'application/x-www-form-urlencoded;application/json'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('[HTTP][RESPONSE] upload', Config.baseUrl + params.url, 'statusCode=', res.statusCode, res.data);
|
||||
typeof params.success == 'function' && params.success(JSON.parse(res.data));
|
||||
},
|
||||
fail: (res) => {
|
||||
console.error('[HTTP][FAIL] upload', Config.baseUrl + params.url, JSON.stringify(res));
|
||||
typeof params.fail == 'function' && params.fail(res);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1247,7 +1247,7 @@ export default {
|
||||
const fieldCacheKey = `${locale}__${langPath}__${_overrideVersion}__${field}`;
|
||||
const cachedFieldResult = _fieldResultCache[fieldCacheKey];
|
||||
const hasDefault = arguments.length > 1 && typeof arguments[1] === 'string' && arguments[1] !== '';
|
||||
|
||||
const __DEV__ = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production');
|
||||
if (cachedFieldResult !== undefined) {
|
||||
let value = cachedFieldResult;
|
||||
// 没找到翻译
|
||||
@@ -1256,6 +1256,10 @@ export default {
|
||||
if (hasDefault) {
|
||||
return arguments[1];
|
||||
}
|
||||
// 生产环境无默认值 → 返回空串(避免用户看到 raw key)
|
||||
if (!__DEV__) {
|
||||
return '';
|
||||
}
|
||||
// 开发环境 → 返回 raw key(便于发现问题)
|
||||
return value;
|
||||
}
|
||||
@@ -1346,6 +1350,9 @@ export default {
|
||||
// 有默认值 → 使用默认值
|
||||
if (hasDefault) {
|
||||
value = arguments[1];
|
||||
} else if (!__DEV__) {
|
||||
// 生产环境无默认值 → 返回空串(避免用户看到 raw key)
|
||||
value = '';
|
||||
}
|
||||
// 开发环境无默认值 → 保留 raw key(便于发现问题)
|
||||
} else if (arguments.length > 1) {
|
||||
@@ -1360,7 +1367,7 @@ export default {
|
||||
if (value == undefined) value = '';
|
||||
|
||||
// 多语言调试
|
||||
if (field == value) {
|
||||
if (__DEV__ && field == value) {
|
||||
console.warn(`[i18n] missing key "${field}" in ${langPath || '<root>'} (${locale})`);
|
||||
}
|
||||
|
||||
|
||||
@@ -147,13 +147,20 @@ function getQuickAppPrivacySetting() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 鸿蒙元服务: 已接入平台隐私托管, 应用层不再调任何系统 API
|
||||
* 仅为兼容历史代码保留此函数 (被 checkPrivacy() 调用), 内部只走 storage 路径
|
||||
* @returns {Promise<{needAuthorization: boolean}>}
|
||||
* 鸿蒙元服务: 查询平台级隐私设置
|
||||
* 鸿蒙元服务的隐私授权由系统自动管理,应用层只需检查 storage
|
||||
* @returns {Promise<{needAuthorization: boolean, privacyContractName?: string}>}
|
||||
*/
|
||||
function getHarmonyPrivacySetting() {
|
||||
return new Promise((resolve) => {
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙元服务: 隐私授权由系统管理,应用层仅检查 storage
|
||||
// 系统会在用户首次使用隐私API时自动弹出系统级隐私授权弹窗
|
||||
resolve({ needAuthorization: !getStorageAgreed() });
|
||||
// #endif
|
||||
// #ifndef MP-HARMONY
|
||||
resolve({ needAuthorization: !getStorageAgreed() });
|
||||
// #endif
|
||||
});
|
||||
}
|
||||
|
||||
@@ -181,7 +188,7 @@ async function checkPrivacy() {
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 鸿蒙元服务: 调系统 getPrivacySetting (无 API 时 getHarmonyPrivacySetting 内部回退 storage)
|
||||
// 鸿蒙元服务: 隐私由系统管理,仅查 storage
|
||||
// H5 / 其他平台: 仅查 storage
|
||||
return checkPrivacySync();
|
||||
}
|
||||
@@ -723,9 +730,8 @@ const PrivacyManager = {
|
||||
|
||||
// 优化: storage 已同意时直接走同步快速路径, 不调 async checkPrivacy()
|
||||
// 节省 1 个 microtask (~0-4ms, dev 模式更明显)
|
||||
// 注意: 微信小程序/快应用走 checkPrivacy (平台 API 异步)
|
||||
// 注意: 微信小程序/快应用/鸿蒙走 checkPrivacy (平台 API 异步)
|
||||
// H5 下 checkPrivacy 内部已 sync 化 (修过), 直接 await 即可
|
||||
// 鸿蒙元服务: 不调任何系统 API, 不弹自建弹窗 (走平台托管)
|
||||
const result = _storageAgreed ? checkPrivacySync() : await checkPrivacy();
|
||||
|
||||
_needShow = result.needAuthorization;
|
||||
@@ -735,18 +741,7 @@ const PrivacyManager = {
|
||||
return;
|
||||
}
|
||||
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙元服务: 已接入平台隐私托管, 由鸿蒙系统自动弹出系统级隐私弹窗
|
||||
// 应用层不再 emit 'privacy:show' 触发自建 privacy-popup.vue
|
||||
// 标记已决策, 防止 consumePendingShow 兜底分支在 page mounted 时再次弹自建弹窗
|
||||
_needShow = false;
|
||||
_pendingShow = false;
|
||||
_urlRuleDecided = true;
|
||||
console.log('[privacy] MP-HARMONY: 已委托鸿蒙平台隐私托管, 不触发自建弹窗');
|
||||
return;
|
||||
// #endif
|
||||
|
||||
// 其他平台: 设置待弹窗标志 + emit 触发自建弹窗
|
||||
// 设置待弹窗标志
|
||||
// 已 mounted 的页面组件通过事件接收
|
||||
// 未 mounted 的页面组件 mounted 时通过 consumePendingShow 补偿
|
||||
_pendingShow = true;
|
||||
@@ -762,28 +757,18 @@ const PrivacyManager = {
|
||||
* 页面 mounted 时补偿检查: 若 show() 在页面 mounted 之前 emit 了事件
|
||||
* 此方法返回 true 并清除标志,页面组件据此自动弹窗
|
||||
*
|
||||
* 历史兼容:
|
||||
* 鸿蒙元服务兼容:
|
||||
* Lang.boot() 可能 hang, PrivacyManager.show() 可能从未被调用过;
|
||||
* 此时 _pendingShow 仍是默认 false, 但用户从未同意过隐私, 必须强制弹一次。
|
||||
* 所以增加 storage 检测: 用户未同意也返回 true (补偿弹窗)。
|
||||
*
|
||||
* 鸿蒙元服务(双轨制):
|
||||
* 已接入平台隐私托管, 由鸿蒙系统自动弹出系统级隐私弹窗,
|
||||
* 应用层不再 emit/弹自建弹窗, 兜底分支也必须返回 false, 否则会与
|
||||
* 鸿蒙系统级弹窗叠加出现"多次弹出"。
|
||||
* @returns {boolean} true=需要弹窗,false=不需要
|
||||
*/
|
||||
consumePendingShow() {
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙元服务: 已接入平台隐私托管, 不弹自建弹窗
|
||||
return false;
|
||||
// #endif
|
||||
|
||||
if (_pendingShow && _needShow) {
|
||||
_pendingShow = false;
|
||||
return true;
|
||||
}
|
||||
// 兜底: PrivacyManager.show() 没跑过 + 用户没同意过 storage → 必须弹
|
||||
// 鸿蒙兜底: PrivacyManager.show() 没跑过 + 用户没同意过 storage → 必须弹
|
||||
// 同一次进程内只补一次,避免隐私弹窗被反复触发
|
||||
// 关键: URL/路由规则已经决策过 "本次跳过" 时, 不应触发兜底弹窗 (白名单场景)
|
||||
if (!_hasChecked && !_pendingShow && !_urlRuleDecided && !getStorageAgreed()) {
|
||||
|
||||
@@ -33,16 +33,7 @@ function useNativeLoading() {
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 总兜底超时档位 (show 时可覆盖):
|
||||
// 12000 (12s): 默认档, 启动体验优先 (大多数网络环境够用, 略比 10s 宽松)
|
||||
// 17000 (17s): 中档, 慢网/冷启动 (1s+ 启动 + 3-5s i18n 远端 + 1-3s diy 拉取 + 缓冲)
|
||||
// 22000 (22s): 慢网档, 极弱网环境 (1s+ 启动 + 5-8s i18n 远端 + 3-5s diy 拉取 + 缓冲)
|
||||
// 低于 12000 不推荐: 用户感知的"启动卡"窗口期不够, 但业务方仍可传 < 12000 测试用
|
||||
// 业务方覆盖: splashState.show({ timeout: 17000 })
|
||||
// 注意: DEFAULT_TIMEOUT 仍是兜底 (无 opts.timeout), 不能被业务方传更低
|
||||
// 严防因为 timeout 太短导致用户看到白屏.
|
||||
// 设计思路: 各档位比"刚好够用"多 2s 缓冲, 弱网/慢网络环境兜底更稳
|
||||
const DEFAULT_TIMEOUT = 12000; // 总兜底超时 (默认 12s)
|
||||
const DEFAULT_TIMEOUT = 8000; // 总兜底超时
|
||||
const MOUNT_GRACE_MS = 1000; // 软隐藏后等组件 mount 的宽限时间
|
||||
const PAGE_READY_GRACE_MS = 2000; // 软隐藏后等页面就绪的宽限时间
|
||||
|
||||
@@ -80,111 +71,6 @@ class SplashState {
|
||||
// 避免多通道并发时, splash 组件 re-render 风暴
|
||||
this._pendingProgress = null; // 待 emit 的最新进度
|
||||
this._flushTimer = null; // 节流定时器
|
||||
// 网络环境 (用于 show() 自动选档)
|
||||
// _networkType: 'wifi' | '4g' | '5g' | '3g' | '2g' | 'none' | 'unknown'
|
||||
// _networkReady: false 表示还没检测完 (splash 显示中第一次构造时常见)
|
||||
// _networkReady 后, 网络变化时通过 onNetworkStatusChange 实时更新
|
||||
this._networkType = 'unknown';
|
||||
this._networkReady = false;
|
||||
|
||||
// 启动网络检测 (异步, 不阻塞 splash 显示)
|
||||
this._initNetworkDetect();
|
||||
|
||||
// 监听 diy 加载失败事件 (diy.js 通过 uni.$emit 触发)
|
||||
// 收到后调 notifyDiyLoadError (严格模式下必须能关闭 splash)
|
||||
// 解耦: splash-state 不直接 import diy.js
|
||||
// 透传 reason / errorType / err / url 给业务方监听 splash:diy-error 使用
|
||||
try {
|
||||
if (typeof uni !== 'undefined' && uni.$on) {
|
||||
uni.$on('diy:load-error', (payload) => {
|
||||
try { this.notifyDiyLoadError(payload || {}) } catch (e) {}
|
||||
})
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化网络检测
|
||||
* - 调 uni.getNetworkType 异步获取当前网络类型
|
||||
* - 监听 uni.onNetworkStatusChange 实时更新
|
||||
* - H5 平台降级: 调 navigator.onLine, 不支持细粒度网络类型
|
||||
* 失败保护: try-catch 包, 任意失败都保持 _networkType='unknown' (不会破坏 splash)
|
||||
*/
|
||||
_initNetworkDetect() {
|
||||
try {
|
||||
// #ifdef H5
|
||||
// H5 平台: navigator.onLine 是粗粒度, 只能判断在线/离线
|
||||
// 仅在 navigator.onLine 是明确 boolean (true/false) 时才是 H5
|
||||
// Node 24+ 有 navigator 但 onLine 是 undefined, 不算 H5 (走下面 uni.getNetworkType)
|
||||
if (typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean') {
|
||||
// 初始检测: 用 _updateNetworkType (内部会触发事件)
|
||||
this._updateNetworkType(navigator.onLine ? 'wifi' : 'none', { source: 'h5-initial' })
|
||||
// 监听 online/offline 事件
|
||||
if (typeof window !== 'undefined' && window.addEventListener) {
|
||||
window.addEventListener('online', () => {
|
||||
this._updateNetworkType('wifi', { source: 'h5-online', isConnected: true })
|
||||
})
|
||||
window.addEventListener('offline', () => {
|
||||
this._updateNetworkType('none', { source: 'h5-offline', isConnected: false })
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
// navigator.onLine 不是 boolean (Node/SSR) - 走 uni.getNetworkType
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
if (typeof uni === 'undefined' || !uni.getNetworkType) {
|
||||
// 不可用: 用 _updateNetworkType 设 unknown (会触发事件)
|
||||
this._updateNetworkType('unknown', { source: 'no-uni-api' })
|
||||
return
|
||||
}
|
||||
// 异步获取当前网络类型
|
||||
uni.getNetworkType({
|
||||
success: (res) => {
|
||||
this._updateNetworkType((res && res.networkType) || 'unknown', { source: 'uni-initial' })
|
||||
},
|
||||
fail: () => {
|
||||
this._updateNetworkType('unknown', { source: 'uni-initial-fail' })
|
||||
},
|
||||
})
|
||||
// 监听网络变化
|
||||
if (uni.onNetworkStatusChange) {
|
||||
uni.onNetworkStatusChange((res) => {
|
||||
const newType = (res && res.networkType)
|
||||
|| (res && res.isConnected === false ? 'none' : 'unknown')
|
||||
this._updateNetworkType(newType, {
|
||||
source: 'uni-change',
|
||||
isConnected: res && typeof res.isConnected === 'boolean' ? res.isConnected : undefined,
|
||||
})
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
} catch (e) {
|
||||
// 任意失败都用 unknown, 不影响 splash
|
||||
this._updateNetworkType('unknown', { source: 'init-error' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前网络环境选档
|
||||
* - 离线 (none): 4000 (4s, 让 diy 错误信号有时间触发)
|
||||
* - 弱网 (2g/3g): 22000 (22s 慢网档, 多 2s 缓冲)
|
||||
* - 正常 (4g/5g/wifi): 12000 (12s 默认档, 多 2s 缓冲)
|
||||
* - 未知 (unknown): 17000 (17s 中档, 保守)
|
||||
* - 网络未就绪: 12000 (DEFAULT_TIMEOUT, 保守)
|
||||
*/
|
||||
_pickTimeoutByNetwork() {
|
||||
if (!this._networkReady) return DEFAULT_TIMEOUT // 网络检测未就绪, 用默认
|
||||
switch (this._networkType) {
|
||||
case 'none': return 4000 // 离线: 4s 兜底
|
||||
case '2g':
|
||||
case '3g': return 22000 // 弱网: 22s 慢网档
|
||||
case '4g':
|
||||
case '5g':
|
||||
case 'wifi': return 12000 // 正常: 12s 默认档
|
||||
case 'unknown':
|
||||
default: return 17000 // 未知: 17s 中档 (保守)
|
||||
}
|
||||
}
|
||||
|
||||
show(opts = {}) {
|
||||
@@ -225,34 +111,7 @@ class SplashState {
|
||||
try { uni.showLoading({ title: opts.title || '加载中...', mask: false }); } catch (e) {}
|
||||
}
|
||||
|
||||
// 业务方可通过 opts.timeout 覆盖, 但不能低于 5000ms (低于此值给 warn, 仍生效)
|
||||
// 原因: 启动期 i18n/theme/remote-config 远端加载最少 1-2s, diy 加载 1-3s
|
||||
// timeout 低于 5s 会经常触发兜底强关, 用户看到白屏
|
||||
// 测试场景允许传 < 5000ms (因为 mock 环境快), 仍生效但 warn
|
||||
// 优先级: opts.timeout (业务方显式) > _pickTimeoutByNetwork() (自动选档) > DEFAULT_TIMEOUT
|
||||
const MIN_RECOMMENDED_TIMEOUT = 5000
|
||||
let timeout
|
||||
if (opts.timeout) {
|
||||
timeout = opts.timeout
|
||||
if (opts.timeout < MIN_RECOMMENDED_TIMEOUT) {
|
||||
console.warn(
|
||||
`[splash-state] timeout=${opts.timeout}ms 低于推荐下限 ${MIN_RECOMMENDED_TIMEOUT}ms, ` +
|
||||
`可能导致启动期白屏. 生产环境建议 10000-20000ms.`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 自动选档: 根据当前网络环境选 timeout
|
||||
timeout = this._pickTimeoutByNetwork()
|
||||
}
|
||||
this._armTimeout(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置/重设 splash 总超时兜底
|
||||
* - 由 show() 初始化, 或 _updateNetworkType() 在网络变化时调用
|
||||
* - 关键: 网络从好→差时延长, 差→好时缩短, 保证用户不会过早/过晚看到白屏
|
||||
*/
|
||||
_armTimeout(timeout) {
|
||||
const timeout = opts.timeout || DEFAULT_TIMEOUT;
|
||||
if (this._timeoutId) {
|
||||
clearTimeout(this._timeoutId);
|
||||
this._timeoutId = null;
|
||||
@@ -266,55 +125,6 @@ class SplashState {
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络变化时更新 _networkType, 并按需重启 splash 兜底
|
||||
* @param {string} newType 新的网络类型
|
||||
* @param {Object} [meta] 附加信息 { isConnected, source: 'h5-online' | 'uni-change' | ... }
|
||||
* @returns {boolean} 是否真的更新了 (新旧不同时为 true)
|
||||
*/
|
||||
_updateNetworkType(newType, meta) {
|
||||
newType = newType || 'unknown'
|
||||
const oldType = this._networkType
|
||||
const changed = oldType !== newType
|
||||
// 关键: 即使 type 没变, _networkReady 也要设 true
|
||||
// 原因: 首次调用 _updateNetworkType 是从 _initNetworkDetect 进来
|
||||
// 此时 _networkType='unknown' (初始), 可能 newType 也是 'unknown' (uni 不可用)
|
||||
// 但 _networkReady 仍要从 false → true (让 _pickTimeoutByNetwork 工作)
|
||||
this._networkReady = true
|
||||
if (!changed) return false
|
||||
this._networkType = newType
|
||||
|
||||
// 网络变化时: 如果 splash 还 visible, 重启兜底 (用新档)
|
||||
// 业务场景:
|
||||
// - wifi → 2g: 启动期远端慢, 兜底延长, 避免过早强关
|
||||
// - 2g → wifi: 兜底缩短, 避免过晚强关
|
||||
// - any → none: 兜底缩短到 4s, 让 diy 错误信号更快触发
|
||||
// - none → any: 兜底恢复正常档, 给足时间
|
||||
const shouldReboot = this._visible && this._pendingHide === null
|
||||
// 关键: 仅当 splash 还没进入"软隐藏"阶段时重启兜底
|
||||
// _pendingHide 不为 null 表示业务方已调 hide(), 等 notifyPageReady/DiyLoadError 即可
|
||||
// 此时网络变化不影响兜底 (业务方已接管决策)
|
||||
if (shouldReboot) {
|
||||
const newTimeout = this._pickTimeoutByNetwork()
|
||||
this._armTimeout(newTimeout)
|
||||
}
|
||||
|
||||
// 发 splash:network-change 事件 (业务方监听)
|
||||
// payload: { from, to, isConnected, retryRecommended, rebootSplashTimeout }
|
||||
// retryRecommended: 从 none 恢复时建议重试 (业务方重调 diy 接口)
|
||||
// rebootSplashTimeout: 是否重启了 splash 兜底 (true/false)
|
||||
const eventPayload = {
|
||||
from: oldType,
|
||||
to: newType,
|
||||
isConnected: meta && typeof meta.isConnected === 'boolean' ? meta.isConnected : newType !== 'none',
|
||||
retryRecommended: oldType === 'none' && newType !== 'none', // 离线→在线, 建议重试
|
||||
rebootSplashTimeout: shouldReboot,
|
||||
source: (meta && meta.source) || 'unknown',
|
||||
}
|
||||
this._emit('splash:network-change', eventPayload)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏 splash
|
||||
* @param {Object} opts
|
||||
@@ -326,22 +136,15 @@ class SplashState {
|
||||
* @param {string} [opts.mode] - 离开动画模式
|
||||
* - 'default': 默认 480ms 动画
|
||||
* - 'fast': 80ms 短动画, 用于隐私弹窗等需要立即接管视觉的场景, 避免"双遮罩"感
|
||||
* - 'strict': 严格模式, 不走 MOUNT_GRACE_MS / PAGE_READY_GRACE_MS 兜底,
|
||||
* 只等 notifyPageReady() 或 DEFAULT_TIMEOUT 总超时
|
||||
* 适用于: 已接入平台托管的系统弹窗 (鸿蒙),
|
||||
* splash 必须真等到首页 diy 内容渲染好才能关
|
||||
*/
|
||||
hide(opts = {}) {
|
||||
const reason = opts.reason || 'manual';
|
||||
const useStrict = opts.mode === 'strict' || opts._strict === true;
|
||||
// boot-complete 默认走软隐藏, 避免时序问题
|
||||
const useSoft = useStrict || opts.soft === true || (opts.soft !== false && reason === 'boot-complete');
|
||||
// strict 模式复用 fast 动画
|
||||
const mode = useStrict ? 'fast' : (opts.mode === 'fast' ? 'fast' : 'default');
|
||||
const useSoft = opts.soft === true || (opts.soft !== false && reason === 'boot-complete');
|
||||
const mode = opts.mode === 'fast' ? 'fast' : 'default';
|
||||
|
||||
if (useSoft) {
|
||||
// 传递完整 opts, _softHide 内部能识别 mode='strict' 或 _strict
|
||||
this._softHide(opts);
|
||||
this._softHide({ reason, mode });
|
||||
} else {
|
||||
this._doHide({ reason, mode });
|
||||
}
|
||||
@@ -351,17 +154,10 @@ class SplashState {
|
||||
* 软隐藏: 标记应该 hide, 等组件 mount + 页面就绪
|
||||
* 如果组件 1s 内没 mount, 兜底自动 hide
|
||||
* 如果页面 2s 内没就绪, 兜底自动 hide
|
||||
*
|
||||
* strict 模式 (鸿蒙已接入平台托管):
|
||||
* 不走 MOUNT_GRACE_MS / PAGE_READY_GRACE_MS 兜底
|
||||
* 只等 notifyMounted() + notifyPageReady() 或 DEFAULT_TIMEOUT 总超时
|
||||
* 保证 splash 一定等到首页 diy 内容真的渲染好
|
||||
*/
|
||||
_softHide(opts) {
|
||||
if (!this._visible) return; // 已经隐藏了
|
||||
this._pendingHide = opts;
|
||||
// strict 模式识别: mode='strict' 或 _strict=true
|
||||
const useStrict = opts && (opts.mode === 'strict' || opts._strict === true);
|
||||
|
||||
// 如果组件已经 mount 且页面已就绪, 立即隐藏
|
||||
if (this._hasMounted && this._pageReady) {
|
||||
@@ -369,12 +165,6 @@ class SplashState {
|
||||
return;
|
||||
}
|
||||
|
||||
// strict 模式: 不设 mountGraceId 兜底, 只等 notifyPageReady
|
||||
// (DEFAULT_TIMEOUT 在 show() 设置的 _timeoutId 兜底)
|
||||
if (useStrict) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 兜底: 1s 内没 mount, 强制隐藏
|
||||
if (this._mountGraceId) clearTimeout(this._mountGraceId);
|
||||
this._mountGraceId = setTimeout(() => {
|
||||
@@ -387,27 +177,15 @@ class SplashState {
|
||||
/**
|
||||
* 真正触发软隐藏
|
||||
* 如果页面还未就绪, 等待页面就绪后再 hide
|
||||
* strict 模式 (鸿蒙): 不设 pageReadyGraceId 兜底, 只等 notifyPageReady()
|
||||
*/
|
||||
_flushPendingHide() {
|
||||
if (this._mountGraceId) {
|
||||
clearTimeout(this._mountGraceId);
|
||||
this._mountGraceId = null;
|
||||
}
|
||||
// 已被 notifyDiyLoadError 拿走 _pendingHide 并自己 _doHide: noop
|
||||
// 原因: notifyMounted 50ms 后会调 _flushPendingHide, 但 _pendingHide 已被 notifyDiyLoadError 消费
|
||||
// 不应再调 _doHide (避免重复发 splash:hide 或时序错乱)
|
||||
if (!this._pendingHide) return;
|
||||
const _opts = this._pendingHide
|
||||
const useStrict = _opts && (_opts.mode === 'strict' || _opts._strict === true)
|
||||
|
||||
// 如果页面还未就绪, 等待页面就绪 (保留 _pendingHide, 等页面就绪后再处理)
|
||||
if (!this._pageReady) {
|
||||
// strict 模式: 不设 pageReadyGraceId, 只等 notifyPageReady()
|
||||
// 总超时由 show() 里的 _timeoutId 兜底 (DEFAULT_TIMEOUT=8s)
|
||||
if (useStrict) {
|
||||
return;
|
||||
}
|
||||
// 兜底: 2s 内没就绪, 强制隐藏
|
||||
if (this._pageReadyGraceId) clearTimeout(this._pageReadyGraceId);
|
||||
this._pageReadyGraceId = setTimeout(() => {
|
||||
@@ -426,11 +204,6 @@ class SplashState {
|
||||
|
||||
/**
|
||||
* 实际隐藏 (硬路径)
|
||||
* @param {Object} opts
|
||||
* @param {string} [opts.reason='manual'] 隐藏原因
|
||||
* @param {string} [opts.mode='default'] 动画模式
|
||||
* @param {boolean} [opts._silent=false] true=不发 splash:hide 事件
|
||||
* 适用于 notifyDiyLoadError: 已经发了 splash:diy-error, 不能再发 splash:hide
|
||||
*/
|
||||
_doHide(opts) {
|
||||
if (!this._visible) return; // 防重入
|
||||
@@ -452,8 +225,6 @@ class SplashState {
|
||||
if (useNativeLoading()) {
|
||||
try { uni.hideLoading(); } catch (e) {}
|
||||
}
|
||||
// _silent=true: 业务方已经从 notifyDiyLoadError 收了 splash:diy-error, 这里不发 splash:hide
|
||||
if (opts._silent) return;
|
||||
this._emit('splash:hide', {
|
||||
reason: opts.reason || 'manual',
|
||||
mode: opts.mode || 'default',
|
||||
@@ -493,60 +264,6 @@ class SplashState {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* diy 加载失败时通知 (网络异常/业务失败/无响应)
|
||||
* - 任何模式: 真关 splash (不能无限等, 错误已显示在页面横幅上)
|
||||
* - 触发独立事件 'splash:diy-error' (不是 'splash:hide')
|
||||
* 业务方可监听此事件做: 错误上报 / 自定义错误展示 / 区分错误类型
|
||||
* 调用时机: diy 错误横幅出现后 (diy.js._showDiyLoadError)
|
||||
*
|
||||
* @param {Object} [opts]
|
||||
* @param {string} [opts.reason='diy-load-error'] 隐藏原因
|
||||
* 用于 'splash:diy-error' 事件的 payload, 业务方可区分:
|
||||
* - 'diy-network-error': 网络异常 (uni.request catch)
|
||||
* - 'diy-no-response': 无响应 (res 为空)
|
||||
* - 'diy-business-error': 业务失败 (code!=0)
|
||||
* @param {string} [opts.errorType] 错误类型: 'network' | 'business' | 'no-response'
|
||||
* @param {Object|string} [opts.err] 原始错误对象
|
||||
* @param {string} [opts.url] 出错的请求地址
|
||||
*/
|
||||
notifyDiyLoadError(opts = {}) {
|
||||
// 已 _pageReady=true (notifyPageReady 之后): noop
|
||||
// 原因: 业务方先调 notifyPageReady (diy 渲染好) 之后又调 notifyDiyLoadError (重复调用或时序错乱)
|
||||
// 不应再触发 splash:diy-error (已经发过 splash:hide)
|
||||
if (this._pageReady) return;
|
||||
// 复用 notifyPageReady 的关闭逻辑 (页面就绪 = 错误状态也算"就绪")
|
||||
// 原因: 错误状态页 (diyLoadError=true) 已经有 UI 反馈, splash 真关不卡视觉
|
||||
this._pageReady = true
|
||||
// 清除所有兜底, 真关 (strict 模式严格依赖 notifyPageReady/notifyDiyLoadError)
|
||||
if (this._pageReadyGraceId) {
|
||||
clearTimeout(this._pageReadyGraceId)
|
||||
this._pageReadyGraceId = null
|
||||
}
|
||||
if (this._mountGraceId) {
|
||||
clearTimeout(this._mountGraceId)
|
||||
this._mountGraceId = null
|
||||
}
|
||||
// 触发独立事件 (不发 splash:hide, 让业务方区分正常 hide 和 diy 错误)
|
||||
this._emit('splash:diy-error', {
|
||||
reason: opts.reason || 'diy-load-error',
|
||||
errorType: opts.errorType,
|
||||
err: opts.err,
|
||||
url: opts.url,
|
||||
})
|
||||
// 关键: 直接 _doHide 关闭 splash, **不**走 _flushPendingHide
|
||||
// 原因: _flushPendingHide 内部会 _doHide → 触发 splash:hide 事件
|
||||
// notifyDiyLoadError 路径**只**应发 splash:diy-error, 不应发 splash:hide
|
||||
// (业务方根据事件类型区分, 不能混淆)
|
||||
if (this._pendingHide) {
|
||||
// 把 _pendingHide 取出, 直接 _doHide 关闭
|
||||
// _silent=true 让 _doHide 不发 splash:hide (因为已经发了 splash:diy-error)
|
||||
const _pendingOpts = this._pendingHide
|
||||
this._pendingHide = null
|
||||
setTimeout(() => this._doHide({ ..._pendingOpts, _silent: true }), 50)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件 unmount 时通知 (用于 _hasMounted 状态管理)
|
||||
* 目前保留 _hasMounted=true, 因为一般只有 tab 页有 splash 组件, 不会动态增减
|
||||
|
||||
@@ -702,10 +702,18 @@ export function _safeEncodeForPlatform(v) {
|
||||
// 微信小程序: split on '&' / '=' 解析 url,但不 decode value 给 onLoad。
|
||||
// - 中文 / 字母 / 数字 → 原样保留(否则 onLoad 拿到 %xx 乱码)
|
||||
// - `&` `=` `?` `#` 空格 → 必须 encode(否则 query 边界被破坏, src 等含 URL 的字段被截断,webview 打不开)
|
||||
if (['mp-weixin','mp-harmony'].includes(platform)) {
|
||||
if (platform === 'mp-weixin') {
|
||||
return _encodeUrlDelimiters(once);
|
||||
}
|
||||
|
||||
// 鸿蒙元服务: uni.navigateTo 内部整体 encode url(把中文、& 等都 encode)。
|
||||
// JS 侧完全不 encode,直接返回 decode roundtrip 后的原文,让鸿蒙原生做唯一一次 encode。
|
||||
// (再 encode 会双重编码 → onLoad 拿到 %E6%B2%99%E5%8F%91 乱码)
|
||||
// webview URL 含 & 时, JS 不 encode,鸿蒙原生 encode 后 & 变 %26,webview 能正常打开。
|
||||
if (platform === 'mp-harmony') {
|
||||
return once;
|
||||
}
|
||||
|
||||
// H5 / 华为快应用: 浏览器不自动编码, JS 侧必须主动 encode 一次(标准 encodeURIComponent)
|
||||
return encodeURIComponent(once);
|
||||
}
|
||||
|
||||
@@ -102,36 +102,6 @@ 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 CATEGORY_PAGE_URL = '/pages_goods/category';
|
||||
@@ -487,9 +457,6 @@ export default {
|
||||
/** 把 page_params 还原成对象(JSON.parse 的安全封装) */
|
||||
parsePageParams,
|
||||
|
||||
/** 把任意值强转为布尔值,专治 URL query 参数布尔解析(?show_home=1/'true'/true) */
|
||||
coerceBool,
|
||||
|
||||
/** HTML 实体解码 */
|
||||
decodeEntity,
|
||||
|
||||
@@ -544,27 +511,6 @@ export default {
|
||||
// 注意: URL 清理只在 diyRedirectTo 入口做(更上游,职责清晰)。
|
||||
// redirectTo 接收到的 param 应当是干净的;不重复清理,避免性能浪费 + 误伤边界。
|
||||
//
|
||||
// 0. 智能默认 mode (2026-07-23, 修正 2026-07-23):
|
||||
// 跳登录入口 LOGIN_PAGE_URL 时, 默认使用 reLaunch 模式 (即使调用方没传 mode).
|
||||
// 修复场景:
|
||||
// - 注销 / 退出登录后, token 被清空, cancelstatus.vue 等页面再次进入
|
||||
// 会调 redirectTo(LOGIN_PAGE_URL), 历史上走 navigateTo 模式, 旧页面
|
||||
// (cancelstatus) 还在栈中, 用户按返回键回到 cancelstatus 后又触发
|
||||
// onShow 死循环跳登录页.
|
||||
// 排除场景 (重要, 避免影响):
|
||||
// - 带 back 参数的: 表示登录后要回到原页面 (订单详情/退款详情等),
|
||||
// 必须保留 navigateTo 模式让 back 参数生效. 例如:
|
||||
// refund_detail.vue: redirectTo(LOGIN_PAGE_URL, { back: '/pages_tool/order/refund_detail?id=xxx' })
|
||||
// - 显式传 'navigateTo' / 'redirectTo' / 其他 mode: 保持原意.
|
||||
// 触发条件: mode 是默认 'navigateTo' AND 没传 back 参数 AND 目标是 LOGIN_PAGE_URL.
|
||||
if (mode === 'navigateTo' && typeof to === 'string' && !param?.back) {
|
||||
const cleanPath = to.indexOf('?') === -1 ? to : to.substring(0, to.indexOf('?'));
|
||||
if (cleanPath === LOGIN_PAGE_URL) {
|
||||
mode = 'reLaunch';
|
||||
console.log('[navigate] redirectTo: 跳 LOGIN_PAGE_URL 自动改为 reLaunch 模式 (没带 back 参数)');
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 拆分 path 与已有 query,避免后面 indexOf('?') 误判
|
||||
console.log('[navigate] redirectTo entry: to=', to, 'mode=', mode, 'param=', JSON.stringify(param));
|
||||
|
||||
@@ -592,18 +538,18 @@ export default {
|
||||
merged.page_params = page_params;
|
||||
}
|
||||
|
||||
// 3. 参数编码策略(委托给 util-navigate 的 _safeEncodeForPlatform)
|
||||
// - 微信小程序 + 鸿蒙元服务(mp-weixin/mp-harmony):uni.navigateTo / router.pushUrl 内部会自动对 url 做一次编码,
|
||||
// JS 侧再做 encodeURIComponent 会被平台再编码一层,目标页 onLoad(options) 拿到的
|
||||
// 是已编码字符串(典型表现:商品分类 → 商品列表,
|
||||
// 搜索栏关键词变成 %E6%B2%99%E5%8F%91 乱码)
|
||||
// => 这里必须保持原始值,完全交给平台原生去编码(_safeEncodeForPlatform
|
||||
// 在 mp-weixin / mp-harmony 下不做 encode,只做 decode roundtrip 去除双向 encode)
|
||||
// - H5 / 华为快应用:浏览器/快应用 Webview 不自动编码 url,
|
||||
// 参数含 & ? = 等特殊字符会被 queryString 错位解析(微信甚至把 & 当
|
||||
// 成 top-level 分隔符丢参数)
|
||||
// => 这里必须手动 encodeURIComponent 一次(_safeEncodeForPlatform 在这些平台下
|
||||
// 做 decode roundtrip + encode 一次)
|
||||
// 3. 参数编码策略(委托给 util-navigate 的 _safeEncodeForPlatform)
|
||||
// - 微信小程序 + 鸿蒙元服务(mp-weixin/mp-harmony):uni.navigateTo / router.pushUrl 内部会自动对 url 做一次编码,
|
||||
// JS 侧再做 encodeURIComponent 会被平台再编码一层,目标页 onLoad(options) 拿到的
|
||||
// 是已编码字符串(典型表现:商品分类 → 商品列表,
|
||||
// 搜索栏关键词变成 %E6%B2%99%E5%8F%91 乱码)
|
||||
// => 这里必须保持原始值,完全交给平台原生去编码(_safeEncodeForPlatform
|
||||
// 在 mp-weixin / mp-harmony 下不做 encode,只做 decode roundtrip 去除双向 encode)
|
||||
// - H5 / 华为快应用:浏览器/快应用 Webview 不自动编码 url,
|
||||
// 参数含 & ? = 等特殊字符会被 queryString 错位解析(微信甚至把 & 当
|
||||
// 成 top-level 分隔符丢参数)
|
||||
// => 这里必须手动 encodeURIComponent 一次(_safeEncodeForPlatform 在这些平台下
|
||||
// 做 decode roundtrip + encode 一次)
|
||||
const queryStr = Object.keys(merged)
|
||||
.map(k => _safeEncodeForPlatform(k) + '=' + _safeEncodeForPlatform(merged[k]))
|
||||
.join('&');
|
||||
@@ -669,33 +615,30 @@ export default {
|
||||
const diyTabBar = store.state.tabBarList;
|
||||
if (diyTabBar && diyTabBar.list && Array.isArray(diyTabBar.list)) {
|
||||
const cleanUrl = url.split('?')[0];
|
||||
const isExcludedPage = NO_TAB_PAGES.has(cleanUrl);
|
||||
if (!isExcludedPage) {
|
||||
const isDiyTabRoot = diyTabBar.list.some(item => {
|
||||
if (!item || !item.link || !item.link.wap_url) return false;
|
||||
const normalizedTab = adaptSubpackageUrl(item.link.wap_url.startsWith('/') ? item.link.wap_url : '/' + item.link.wap_url).split('?')[0];
|
||||
const normalizedUrl = adaptSubpackageUrl(cleanUrl.startsWith('/') ? cleanUrl : '/' + cleanUrl).split('?')[0];
|
||||
return normalizedTab === normalizedUrl;
|
||||
const isDiyTabRoot = diyTabBar.list.some(item => {
|
||||
if (!item || !item.link || !item.link.wap_url) return false;
|
||||
const normalizedTab = adaptSubpackageUrl(item.link.wap_url.startsWith('/') ? item.link.wap_url : '/' + item.link.wap_url).split('?')[0];
|
||||
const normalizedUrl = adaptSubpackageUrl(cleanUrl.startsWith('/') ? cleanUrl : '/' + cleanUrl).split('?')[0];
|
||||
return normalizedTab === normalizedUrl;
|
||||
});
|
||||
if (isDiyTabRoot) {
|
||||
const adaptedUrl = adaptSubpackageUrl(cleanUrl);
|
||||
const inSystemTabBar = systemTabBarList.some(sysUrl => adaptedUrl === sysUrl || cleanUrl === sysUrl);
|
||||
// 首页用switchTab,其他DIY tab根页用reLaunch确保页面栈干净
|
||||
let navApi = inSystemTabBar ? (adaptedUrl === INDEX_PAGE_URL ? uni.switchTab : uni.reLaunch) : uni.reLaunch;
|
||||
|
||||
// 鸿蒙元服务环境下 uni.switchTab 有兼容性问题, 全部使用 reLaunch
|
||||
// #ifdef MP-HARMONY
|
||||
navApi = uni.reLaunch;
|
||||
// #endif
|
||||
|
||||
navApi({
|
||||
url,
|
||||
fail: (err) => {
|
||||
console.error('[navigate] tabBar 跳转失败:', cleanUrl, err);
|
||||
}
|
||||
});
|
||||
if (isDiyTabRoot) {
|
||||
const adaptedUrl = adaptSubpackageUrl(cleanUrl);
|
||||
const inSystemTabBar = systemTabBarList.some(sysUrl => adaptedUrl === sysUrl || cleanUrl === sysUrl);
|
||||
// 首页用switchTab,其他DIY tab根页用reLaunch确保页面栈干净
|
||||
let navApi = inSystemTabBar ? (adaptedUrl === INDEX_PAGE_URL ? uni.switchTab : uni.reLaunch) : uni.reLaunch;
|
||||
|
||||
// 鸿蒙元服务环境下 uni.switchTab 有兼容性问题, 全部使用 reLaunch
|
||||
// #ifdef MP-HARMONY
|
||||
navApi = uni.reLaunch;
|
||||
// #endif
|
||||
|
||||
navApi({
|
||||
url,
|
||||
fail: (err) => {
|
||||
console.error('[navigate] tabBar 跳转失败:', cleanUrl, err);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,41 +669,6 @@ export default {
|
||||
break;
|
||||
case 'reLaunch':
|
||||
// 关闭所有页面,打开到应用内的某个页面。清空栈后打开新页面,长度=1
|
||||
// H5 平台修复 (2026-07-23):
|
||||
// uni.reLaunch 在 H5 上是 history.back() + router.replace 的组合,
|
||||
// 并不能真正删除浏览器历史条目。在某些浏览器 (微信内置浏览器 /
|
||||
// Android 定制浏览器) 中, 按浏览器返回键仍可回到原页面, 导致
|
||||
// 注销/退出登录后用户能看到旧会员信息。
|
||||
// location.replace 不产生新历史条目, 直接替换当前条目,
|
||||
// 真正"清空历史栈"。对快应用 (H5 webview 嵌入) 同样生效。
|
||||
// #ifdef H5
|
||||
if (typeof window !== 'undefined' && typeof window.location !== 'undefined'
|
||||
&& typeof window.location.replace === 'function') {
|
||||
window.location.replace(url);
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
// 小程序平台修复 (2026-07-23):
|
||||
// 微信小程序在以下场景会抛 "appLaunch with non-empty page stack" native 错误:
|
||||
// - 短时间内连发 reLaunch (页面销毁未完成, 新 launch 时栈非空)
|
||||
// - 异步回调/onLoad 中调 reLaunch (上下文丢失)
|
||||
// 解决方案:
|
||||
// 1) 跳转锁: 300ms 内不重复 reLaunch (避免连发)
|
||||
// 2) setTimeout(0) 包裹: 让 native 层先完成当前页面销毁, 再发新 launch
|
||||
// #ifdef MP
|
||||
if (typeof globalThis !== 'undefined' && globalThis.__reLaunchLock) {
|
||||
console.warn('[redirectTo] reLaunch 锁定中, 忽略本次跳转:', url);
|
||||
return;
|
||||
}
|
||||
globalThis.__reLaunchLock = true;
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url });
|
||||
// 300ms 后释放锁
|
||||
setTimeout(() => { globalThis.__reLaunchLock = false; }, 300);
|
||||
}, 0);
|
||||
return;
|
||||
// #endif
|
||||
// APP / 其他平台: 原生 reLaunch
|
||||
uni.reLaunch({
|
||||
url
|
||||
});
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
<template>
|
||||
<!-- 修复底部 TabBar 频闪:
|
||||
微信小程序等环境下 `shouldShowTabBar` 在 tabBarList 异步加载期间会从 false→true 短暂切换,
|
||||
配合 `v-if` 会让整个 tab 组件树卸载/重建 -> 视觉频闪 + tabBar 占用空间反复占位/释放导致上方内容跳动。
|
||||
改为 `v-show`, 只切换可见性, 内部 tab 始终在 DOM 中(无 tabBarList 时只渲染空壳占位)。 -->
|
||||
<view data-component-name="diy-bottom-nav" v-show="shouldShowTabBar">
|
||||
<!-- tabBarList 未就绪时仍保留 placeholder 占位, 避免上方内容在加载完成后突跳 -->
|
||||
<view v-if="tabBarList && tabBarList.list && tabBarList.list.length" class="tab-bar" :style="{ backgroundColor: tabBarList.backgroundColor }">
|
||||
<view class="tabbar-border"></view>
|
||||
<view class="item" v-for="(item, index) in tabBarList.list" :key="item.id" @tap.stop="redirectTo(item.link)">
|
||||
<view class="bd">
|
||||
<view class="icon" v-if="tabBarList.type == 1 || tabBarList.type == 2"
|
||||
:animation="isCartItem(item.link) ? cartAnimation : null"
|
||||
:id="isCartItem(item.link) ? 'tabbarCart' : null">
|
||||
<block v-if="verify(item.link)">
|
||||
<image v-if="item.selected_icon_type == 'img'"
|
||||
:src="$util.img(item.selectedIconPath)" />
|
||||
<diy-icon v-if="item.selected_icon_type == 'icon'" :icon="item.selectedIconPath"
|
||||
:value="item.selected_style ? item.selected_style : null"></diy-icon>
|
||||
</block>
|
||||
<block v-else>
|
||||
<image v-if="item.icon_type == 'img'" :src="$util.img(item.iconPath)" />
|
||||
<diy-icon v-if="item.icon_type == 'icon'" :icon="item.iconPath"
|
||||
:value="item.style ? item.style : null"></diy-icon>
|
||||
</block>
|
||||
<view class="cart-count-mark font-size-activity-tag"
|
||||
:class="{ max: cartNumber > 99 }"
|
||||
:style="{ background: 'var(--price-color)' }"
|
||||
v-if="isCartItem(item.link) && cartNumber > 0">
|
||||
{{ cartNumber > 99 ? '99+' : cartNumber }}
|
||||
</view>
|
||||
<view data-component-name="diy-bottom-nav" v-if="shouldShowTabBar">
|
||||
<view class="tab-bar" :style="{ backgroundColor: tabBarList.backgroundColor }">
|
||||
<view class="tabbar-border"></view>
|
||||
<view class="item" v-for="(item, index) in tabBarList.list" :key="item.id" @tap.stop="redirectTo(item.link)">
|
||||
<view class="bd">
|
||||
<view class="icon" v-if="tabBarList.type == 1 || tabBarList.type == 2"
|
||||
:animation="isCartItem(item.link) ? cartAnimation : null"
|
||||
:id="isCartItem(item.link) ? 'tabbarCart' : null">
|
||||
<block v-if="verify(item.link)">
|
||||
<image v-if="item.selected_icon_type == 'img'"
|
||||
:src="$util.img(item.selectedIconPath)" />
|
||||
<diy-icon v-if="item.selected_icon_type == 'icon'" :icon="item.selectedIconPath"
|
||||
:value="item.selected_style ? item.selected_style : null"></diy-icon>
|
||||
</block>
|
||||
<block v-else>
|
||||
<image v-if="item.icon_type == 'img'" :src="$util.img(item.iconPath)" />
|
||||
<diy-icon v-if="item.icon_type == 'icon'" :icon="item.iconPath"
|
||||
:value="item.style ? item.style : null"></diy-icon>
|
||||
</block>
|
||||
<view class="cart-count-mark font-size-activity-tag"
|
||||
:class="{ max: cartNumber > 99 }"
|
||||
:style="{ background: 'var(--price-color)' }"
|
||||
v-if="isCartItem(item.link) && cartNumber > 0">
|
||||
{{ cartNumber > 99 ? '99+' : cartNumber }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="label"
|
||||
v-if="(tabBarList.type == 1 || tabBarList.type == 3) && tabBarList.theme == 'diy'"
|
||||
:style="{ color: verify(item.link) ? tabBarList.textHoverColor : tabBarList.textColor }">
|
||||
@@ -43,7 +38,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 解决fixed定位后底部导航栏塌陷问题: 始终保留 placeholder, 避免内容随 tabBar 可见性跳 -->
|
||||
<!-- 解决fixed定位后底部导航栏塌陷问题 -->
|
||||
<view class="tab-bar-placeholder"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-goods-list" v-if="selectedSecondCategory">
|
||||
<view class="goods-list show" :class="{ 'double-column': !isList, 'single-column': isList }" :data-template="value.template" v-if="secondCategoryGoodsList.length">
|
||||
<view class="goods-list double-column show" :data-template="value.template" v-if="secondCategoryGoodsList.length">
|
||||
<view class="goods-item" v-for="(item, index) in secondCategoryGoodsList" :key="index">
|
||||
<view class="goods-img" @tap.stop="toDetail(item)">
|
||||
<image :src="goodsImg(item.goods_image)" mode="widthFix" @error="imgError(index)" show-menu-by-longpress />
|
||||
@@ -35,13 +35,6 @@
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
<view class="footer-wrap">
|
||||
<view class="consult-now-btn-wrap" v-if="item.isinformation == 1">
|
||||
<view class="consult-now-btn" @tap.stop="sendToAIChat(item)">
|
||||
{{ $lang('components-diy.diy-category-item.consultNow') }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -58,7 +51,7 @@
|
||||
<view :class="['category-title', 'category-title-with-lines']">{{ one.category_name }}</view>
|
||||
<view class="template1-goods-wrap">
|
||||
<view class="goods-list double-column show" v-if="template1ChildGoodsList[one.category_id] && template1ChildGoodsList[one.category_id].length">
|
||||
<view class="goods-item" v-for="(item, index) in template1ChildGoodsList[one.category_id]" :key="index">
|
||||
<view class="goods-item" v-for="(item, index) in template1ChildGoodsList[one.category_id]" :key="one.category_id + '-' + index">
|
||||
<view class="goods-img" @tap.stop="toDetail(item)">
|
||||
<image :src="goodsImg(item.goods_image)" mode="widthFix" @error="template1ImgError(one.category_id, index)" show-menu-by-longpress />
|
||||
<view class="color-base-bg goods-tag" v-if="item.label_name">{{ item.label_name }}</view>
|
||||
@@ -76,9 +69,6 @@
|
||||
</block>
|
||||
</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>
|
||||
@@ -113,9 +103,6 @@
|
||||
</block>
|
||||
</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>
|
||||
@@ -179,9 +166,6 @@
|
||||
</block>
|
||||
</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>
|
||||
@@ -525,7 +509,7 @@ export default {
|
||||
currentOneCategory: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
}
|
||||
},
|
||||
mixins: [DiyMinx],
|
||||
data() {
|
||||
@@ -591,7 +575,7 @@ export default {
|
||||
*/
|
||||
isGroupedDisplay() {
|
||||
return this.value.template == 1 && this.value.level == 3;
|
||||
},
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.type = this.value.template == 1 || this.value.template == 99 ? 'category' : 'goods';
|
||||
@@ -1429,26 +1413,6 @@ export default {
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2417,29 +2381,25 @@ export default {
|
||||
.second-category-btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 20rpx 20rpx 20rpx 24rpx;
|
||||
height: 100rpx;
|
||||
padding: 20rpx 20rpx 10rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
box-sizing: border-box;
|
||||
|
||||
.second-category-btn {
|
||||
padding: 10rpx 24rpx;
|
||||
padding: 14rpx 32rpx;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
border-radius: 30rpx;
|
||||
font-size: 20rpx;
|
||||
margin: 0 14rpx 0rpx 0;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
margin: 0 16rpx 16rpx 0;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.selected {
|
||||
background: #FF4646;
|
||||
color: #FFFFFF;
|
||||
background: #FF6B35;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2535,99 +2495,6 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
&.single-column {
|
||||
flex-direction: column;
|
||||
padding: 0 20rpx 0 8rpx;
|
||||
gap: 20rpx;
|
||||
|
||||
.goods-item {
|
||||
width: calc(100% + 20rpx);
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 0;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
height: 220rpx;
|
||||
|
||||
.goods-img {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
padding-top: 0;
|
||||
margin-right: 14rpx;
|
||||
border-radius: 12rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
image {
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.info-wrap {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.name-wrap {
|
||||
.goods-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
-webkit-line-clamp: 2;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
margin-top: 0;
|
||||
white-space: normal;
|
||||
text-overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
.price-wrap {
|
||||
margin-top: 8rpx;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: flex-start;
|
||||
|
||||
.discount-price {
|
||||
color: #FF6B35;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
|
||||
.unit {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.double-column {
|
||||
.goods-item {
|
||||
width: calc(50% - 10rpx);
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,27 +86,13 @@
|
||||
</uni-popup>
|
||||
</view>
|
||||
|
||||
<!-- 全屏骨架占位 (只显示: 左分类树 + 右商品网格 + 底部空隙, 不画 navbar/TabBar,
|
||||
避免与小程序/H5 顶部 navbar / diy-bottom-nav 错位)。
|
||||
通过 v-if 拿到 categoryTreeLoading 时按真实布局骨架填充。
|
||||
search-box / 商品 nav-bar 由外层 category.vue 渲染, 不在本组件里动它 -->
|
||||
<view class="category-page-skeleton" v-if="categoryTreeLoading">
|
||||
<view class="skeleton-content">
|
||||
<view class="skeleton-tree">
|
||||
<view class="skeleton-tree-item" v-for="n in 8" :key="n"></view>
|
||||
</view>
|
||||
<view class="skeleton-grid">
|
||||
<view class="skeleton-card" v-for="n in 6" :key="n"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="content-box" v-if="categoryTree">
|
||||
<block v-if="categoryTree.length">
|
||||
<scroll-view scroll-y="true" class="tree-wrap">
|
||||
<view class="category-item-wrap">
|
||||
<!-- 模板4: 左侧显示一级分类(顶部轮播中的分类) -->
|
||||
<view v-if="value.template == 4">
|
||||
<view class="category-item" v-for="(item, index) in templateFourData" :key="index" :class="[
|
||||
<view class="category-item" v-for="(item, index) in templateFourData" :key="'one-' + index" :class="[
|
||||
{ select: oneCategorySelect == index },
|
||||
{ 'border-bottom': oneCategorySelect + 1 === index },
|
||||
{ 'border-top': oneCategorySelect - 1 === index }
|
||||
@@ -119,8 +105,7 @@
|
||||
<view class="category-item" v-for="(item, index) in categoryTree" :key="index" :class="[
|
||||
{ select: select == index },
|
||||
{ 'border-bottom': value.template == 4 && select + 1 === index },
|
||||
{ 'border-top': value.template == 4 && select - 1 === index },
|
||||
{ 'first-category-item': index == 0 && value.template == 99 }
|
||||
{ 'border-top': value.template == 4 && select - 1 === index }
|
||||
]" @tap.stop="switchOneCategory(index)">
|
||||
<view class="">{{ item.category_name }}</view>
|
||||
</view>
|
||||
@@ -164,7 +149,6 @@
|
||||
<diy-category-item :category="item" :value="value" ref="categoryItem" :index="index"
|
||||
:select="select" :oneCategorySelect="oneCategorySelect" :isList="isList"
|
||||
:lastSecondCategoryName="value.template == 99 ? lastSecondCategoryName : ''"
|
||||
:currentOneCategory="categoryTree && categoryTree[select] ? categoryTree[select] : null"
|
||||
@tologin="toLogin" @selectsku="selectSku($event, index)" @addCart="addCartPoint"
|
||||
@loadfinish="getHeightArea" @saveSecondCategory="saveSecondCategoryName"></diy-category-item>
|
||||
</view>
|
||||
@@ -190,6 +174,10 @@
|
||||
<view class="tips">{{ $lang('components-diy.diy-category.noCategory') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-empty" v-else>
|
||||
<image :src="$util.img('public/uniapp/category/empty.png')" mode="widthFix" show-menu-by-longpress></image>
|
||||
<view class="tips">{{ $lang('components-diy.diy-category.noCategory') }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 模板4: 底部购物车栏 -->
|
||||
<view class="cart-box cart-box-template-4"
|
||||
@@ -212,8 +200,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="cart-point" v-for="(item, index) in carIconList" :key="index"
|
||||
:style="{ left: item.left + 'px', top: item.top + 'px' }"></view>
|
||||
<view class="cart-point" :style="{ left: item.left + 'px', top: item.top + 'px' }" :key="index"
|
||||
v-for="(item, index) in carIconList"></view>
|
||||
|
||||
<ns-goods-sku-category ref="skuSelect" @refresh="refreshData" @addCart="addCartPoint"></ns-goods-sku-category>
|
||||
</view>
|
||||
@@ -261,12 +249,7 @@ export default {
|
||||
isList: false,
|
||||
lang: uni.getStorageSync("lang"),
|
||||
lastSecondCategoryName: '',
|
||||
template99Category: null,
|
||||
// 修复体验: 进入页面初期显示 "暂未分类" 然后又变成有数据的体验很差
|
||||
// 用 categoryTreeLoading 区分"还在请求"和"真的没分类", 请求未完成时
|
||||
// 显示骨架占位而非空态文案
|
||||
categoryTreeLoading: true,
|
||||
|
||||
template99Category: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -303,14 +286,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
// 组件刷新监听
|
||||
componentRefresh: function (nval) { },
|
||||
categoryTreeLoading: function (nval, oval) {
|
||||
if (nval === false && oval === true) {
|
||||
this.$nextTick(() => {
|
||||
if (!this.heightArea.length) this.getHeightArea(-1);
|
||||
});
|
||||
}
|
||||
}
|
||||
componentRefresh: function (nval) { }
|
||||
},
|
||||
computed: {
|
||||
cartTotalMoney() {
|
||||
@@ -384,7 +360,6 @@ export default {
|
||||
this.$store.dispatch('getCartNumber');
|
||||
if (!this.heightArea.length) this.getHeightArea(-1);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取高度区间
|
||||
*
|
||||
@@ -392,13 +367,6 @@ export default {
|
||||
* 加防御性检查,避免上线后第一次进入崩溃
|
||||
*/
|
||||
getHeightArea(index) {
|
||||
// 节流: 500ms 内的重复请求复用上次结果, 避免每次 onShow 都跑 selectAll 触发频闪
|
||||
const now = Date.now();
|
||||
if (this._lastHeightAreaTime && now - this._lastHeightAreaTime < 500 && this.heightArea && this.heightArea.length) {
|
||||
if (index != -1 && index < this.categoryTree.length - 1 && Array.isArray(this.$refs.categoryItem) && this.$refs.categoryItem[index + 1]) this.$refs.categoryItem[index + 1].getGoodsList();
|
||||
return;
|
||||
}
|
||||
this._lastHeightAreaTime = now;
|
||||
let heightArea = [];
|
||||
if (!query || typeof query.selectAll !== 'function') {
|
||||
query = uni.createSelectorQuery && uni.createSelectorQuery().in(this);
|
||||
@@ -428,7 +396,6 @@ export default {
|
||||
* 获取全部分类
|
||||
*/
|
||||
getCategoryTree() {
|
||||
this.categoryTreeLoading = true;
|
||||
this.$api.sendRequest({
|
||||
url: '/api/goodscategory/tree',
|
||||
data: {
|
||||
@@ -458,14 +425,6 @@ export default {
|
||||
this.loadGlobalSecondCategoryBtns();
|
||||
}
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
// 失败也保证 loading 关闭, 让骨架替换为"暂无分类"空态
|
||||
this.categoryTree = this.categoryTree || [];
|
||||
},
|
||||
complete: () => {
|
||||
// 无论成功失败都关闭 loading 骨架
|
||||
this.categoryTreeLoading = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -740,12 +699,11 @@ export default {
|
||||
}
|
||||
|
||||
.content-box {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex: 1;
|
||||
height: 0;
|
||||
display: flex;
|
||||
|
||||
.tree-wrap {
|
||||
.tree-wrap {
|
||||
width: 170rpx;
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
@@ -992,86 +950,6 @@ export default {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 全屏骨架占位 — 不画 navbar/TabBar (由外层 category.vue 渲染), 避免错位
|
||||
// 兼容鸿蒙元服务 (JSVM 对 gap 支持不一): 用 margin 兜底替代 gap
|
||||
.category-page-skeleton {
|
||||
flex: 1;
|
||||
height: 0; // 与 .content-box 一致, 让 flex 子项撑开父容器剩余高度
|
||||
display: flex;
|
||||
background-color: #f4f6fa;
|
||||
|
||||
.skeleton-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
|
||||
.skeleton-tree {
|
||||
width: 170rpx;
|
||||
background-color: #f5f5f5;
|
||||
padding: 20rpx 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// #ifndef MP-HARMONY
|
||||
gap: 18rpx;
|
||||
// #endif
|
||||
flex-shrink: 0;
|
||||
|
||||
.skeleton-tree-item {
|
||||
height: 40rpx;
|
||||
// #ifndef MP-HARMONY
|
||||
margin: 0 28rpx;
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-HARMONY
|
||||
margin: 9rpx 28rpx; // 18rpx 间距 上下各 9rpx, 替代 gap:18rpx
|
||||
// #endif
|
||||
|
||||
border-radius: 12rpx;
|
||||
background: linear-gradient(90deg, #ececec 25%, #f5f5f5 37%, #ececec 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: category-skeleton-shimmer 1.4s ease infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-grid {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
// #ifndef MP-HARMONY
|
||||
gap: 16rpx;
|
||||
// #endif
|
||||
background-color: #f4f6fa;
|
||||
min-width: 0;
|
||||
|
||||
.skeleton-card {
|
||||
width: calc((100% - 32rpx) / 3);
|
||||
height: 220rpx;
|
||||
// #ifdef MP-HARMONY
|
||||
margin: 8rpx;
|
||||
// #endif
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(90deg, #ececec 25%, #f5f5f5 37%, #ececec 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: category-skeleton-shimmer 1.4s ease infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes category-skeleton-shimmer {
|
||||
0% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
}
|
||||
|
||||
// 风格四
|
||||
.category-template-4 {
|
||||
// 模板4 顶部搜索框(图片样式)
|
||||
@@ -1604,21 +1482,18 @@ export default {
|
||||
.tree-wrap .category-item {
|
||||
padding: 30rpx 16rpx;
|
||||
background-color: #f5f5f5;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
|
||||
view {
|
||||
color: #333;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.5;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
&.select {
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
view {
|
||||
color: #333;
|
||||
color: #FF6B35;
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
@@ -1627,7 +1502,7 @@ export default {
|
||||
content: ' ';
|
||||
width: 6rpx;
|
||||
height: 36rpx;
|
||||
background: #FF4646;
|
||||
background: #FF6B35;
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -1638,32 +1513,9 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 模板99: 第一个分类项固定高度,与右侧二级分类按钮区域对齐
|
||||
.tree-wrap .category-item.first-category-item {
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
view {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
background: #f5f5f5;
|
||||
padding: 0;
|
||||
// #ifdef H5
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: transparent;
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
.child-category {
|
||||
|
||||
@@ -265,7 +265,7 @@ export default {
|
||||
// #ifndef MP-WEIXIN
|
||||
if (item?.channelType === 'wechat') {
|
||||
uni.showToast({
|
||||
title: this.$lang('wechatChannel.wechatMpOnly'),
|
||||
title: '视频号仅支持在微信小程序环境中播放',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
<template>
|
||||
<view data-component-name="diy-form" class="diy-from" :style="diyFormStyle">
|
||||
<!-- Banner -->
|
||||
<!-- 鸿蒙元服务运行时在首次 _render 时 value 可能还是 undefined,
|
||||
下面所有 value.xxx 模板表达式都必须用 `value && value.xxx` 守护,
|
||||
否则会触发 MP 编译器注入的 propsToData 观察者(对应编译产物里
|
||||
r=function(){var o=this.value.show_banner...this.$mp.data...})
|
||||
报 "Cannot read properties of undefined (reading 'show_banner')"。 -->
|
||||
<view v-if="value && value.show_banner && value.form_banner" class="form-banner" :style="bannerStyle">
|
||||
<view v-if="value.show_banner && value.form_banner" class="form-banner" :style="bannerStyle">
|
||||
<image :src="$util.img(value.form_banner)" mode="widthFix" class="banner-img" />
|
||||
</view>
|
||||
<!-- 提示语 -->
|
||||
<view v-if="value && value.show_tip && value.form_tip" class="form-tip" :style="tipStyle">{{ value && value.form_tip }}</view>
|
||||
<view v-if="value.show_tip && value.form_tip" class="form-tip" :style="tipStyle">{{ value.form_tip }}</view>
|
||||
<!-- 表单组件 -->
|
||||
<ns-newform
|
||||
:data="formComponents"
|
||||
@@ -22,18 +17,13 @@
|
||||
<!-- 提交按钮(外层 = form-bg + form-padding-x;按钮 = submit-bg + submit-color + input-height + input-radius)。
|
||||
showSubmit=false 时由父级页面渲染自己的提交按钮并通过 ref 调用本组件的 verify()/getFormData()。 -->
|
||||
<view v-if="showSubmit" class="form-submit-wrap" :style="submitWrapStyle">
|
||||
<view @click.stop="handleSubmit" class="form-submit" :style="submitStyle">{{ submitText }}</view>
|
||||
<view @click.stop="submitForm" class="form-submit" :style="submitStyle">{{ submitText }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 自定义表单
|
||||
// 提交接管三档(不依赖 $listeners / 事件机制,跨 H5 / MP / 鸿蒙一致):
|
||||
// 1) 默认 → showSubmit=true + 不传 onSubmit → 内置按钮 + 内置 defaultSubmit
|
||||
// 2) 复用按钮、自定义逻辑 → showSubmit=true + :onSubmit="fn" → 内置按钮 + 父级 fn
|
||||
// 3) 完全自定义 UI → showSubmit=false + :onSubmit="fn" 或 ref.verify()/getFormData() → 父级全控
|
||||
// 注:type: Function prop 在 uni-app MP 端走 Vue runtime 注入,机制与 ns-navbar.customBack 一致
|
||||
import DiyMinx from './minx.js'
|
||||
import NsNewform from '@/components/ns-newform/ns-newform.vue'
|
||||
|
||||
@@ -57,12 +47,6 @@ export default {
|
||||
showSubmit: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 父级可选地接管提交:传入函数 prop 时,verify 通过后由它处理 formData;
|
||||
// 不传时走组件内置 defaultSubmit(兜底)。default 必须是 null,不能写成函数返回 null
|
||||
onSubmit: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
mixins: [DiyMinx],
|
||||
@@ -86,15 +70,13 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
// 鸿蒙元服务运行时首次 _render 时 this.value 可能还是 undefined,必须全程加护
|
||||
var css = '';
|
||||
var v = this.value || {};
|
||||
css += 'background-color:' + (v.contentBgColor || '') + ';';
|
||||
if (v.elementAngle === 'round') {
|
||||
css += 'border-top-left-radius:' + (v.topElementAroundRadius || 0) * 2 + 'rpx;';
|
||||
css += 'border-top-right-radius:' + (v.topElementAroundRadius || 0) * 2 + 'rpx;';
|
||||
css += 'border-bottom-left-radius:' + (v.bottomElementAroundRadius || 0) * 2 + 'rpx;';
|
||||
css += 'border-bottom-right-radius:' + (v.bottomElementAroundRadius || 0) * 2 + 'rpx;';
|
||||
css += 'background-color:' + this.value.contentBgColor + ';';
|
||||
if (this.value.elementAngle === 'round') {
|
||||
css += 'border-top-left-radius:' + this.value.topElementAroundRadius * 2 + 'rpx;';
|
||||
css += 'border-top-right-radius:' + this.value.topElementAroundRadius * 2 + 'rpx;';
|
||||
css += 'border-bottom-left-radius:' + this.value.bottomElementAroundRadius * 2 + 'rpx;';
|
||||
css += 'border-bottom-right-radius:' + this.value.bottomElementAroundRadius * 2 + 'rpx;';
|
||||
}
|
||||
return css;
|
||||
},
|
||||
@@ -159,10 +141,6 @@ export default {
|
||||
submitText() {
|
||||
return this.mergedFormConfig.submitText || '提交';
|
||||
},
|
||||
// 父级是否接管提交(集中判断,便于以后替换机制;避免多个调用点各自 typeof 校验)
|
||||
hasCustomSubmit() {
|
||||
return typeof this.onSubmit === 'function';
|
||||
},
|
||||
// 后端把表单样式配置统一存放在 data.formConfig 子对象中(与后端 design.js getDefaultFormData().formConfig 字段名一致)。
|
||||
// 这里做一次归一化:优先取 formConfig;如果历史数据是扁平结构(直接挂在 value 上),也做兼容。
|
||||
formCfg() {
|
||||
@@ -202,22 +180,11 @@ export default {
|
||||
labelFixedWidth: pick('labelFixedWidth', 'label_fixed_width'),
|
||||
labelFontSize: pick('labelFontSize', 'label_font_size'),
|
||||
labelColor: pick('labelColor', 'label_color'),
|
||||
labelFontWeight: pick('labelFontWeight', 'label_font_weight'),
|
||||
labelAutoMaxWidth: pick('labelAutoMaxWidth', 'label_auto_max_width'),
|
||||
requiredColor: pick('requiredColor', 'required_color'),
|
||||
checkedColor: pick('checkedColor', 'checked_color'),
|
||||
// 占位符:遗漏会被 ns-newform SCSS 默认值回退覆盖
|
||||
placeholderColor: pick('placeholderColor', 'placeholder_color'),
|
||||
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 对齐)
|
||||
inputBorderColor: pick('inputBorderColor', 'input_border_color'),
|
||||
inputBgColor: pick('inputBgColor', 'input_bg_color'),
|
||||
inputFocusBorderColor: pick('inputFocusBorderColor', 'input_focus_border_color'),
|
||||
inputFocusBgColor: pick('inputFocusBgColor', 'input_focus_bg_color'),
|
||||
submitText: pick('submitText', 'submit_text'),
|
||||
submitColor: pick('submitColor', 'submit_color'),
|
||||
submitBgColor: pick('submitBgColor', 'submit_bg_color'),
|
||||
@@ -305,26 +272,24 @@ export default {
|
||||
return true;
|
||||
},
|
||||
|
||||
// 统一提交入口:内置按钮 / 父级 ref 调用都走这里
|
||||
// 不依赖 $listeners / 事件机制(MP 端 $listeners 为 undefined),跨端一致
|
||||
handleSubmit() {
|
||||
// 提交表单
|
||||
submitForm() {
|
||||
var self = this;
|
||||
// 校验(同时返回 formData 数组;校验失败时 ns-newform 内部 toast 提示)
|
||||
if (!this.verify()) {
|
||||
return;
|
||||
}
|
||||
const formData = this.getFormData();
|
||||
// 父级显式接管:function prop 在 H5 / MP / 鸿蒙一致可用
|
||||
if (this.hasCustomSubmit) {
|
||||
return this.onSubmit(formData);
|
||||
}
|
||||
// 兜底:内置 sendRequest 流程
|
||||
this.defaultSubmit(formData);
|
||||
},
|
||||
|
||||
// 公共兜底实现:默认的 /form/api/form/create 流程;
|
||||
// 父级若需"先自定义、失败回退",可经 ref.defaultSubmit(formData) 调用
|
||||
defaultSubmit(submitFormData) {
|
||||
var self = this;
|
||||
// 若父级监听了 @submit,则把提交控制权交给父级(用于父级有自定义提交字段/反馈逻辑的页面,如 formdata.vue 传 uniacid)
|
||||
if (this.$listeners && this.$listeners.submit) {
|
||||
this.$emit('submit', this.getFormData());
|
||||
return;
|
||||
}
|
||||
|
||||
// 获得保单数据
|
||||
const submitFormData = this.getFormData();
|
||||
|
||||
// 提交到后端
|
||||
this.$api.sendRequest({
|
||||
url: '/form/api/form/create',
|
||||
data: {
|
||||
@@ -336,7 +301,7 @@ export default {
|
||||
success: function(res) {
|
||||
if (res.code === 0) {
|
||||
uni.showToast({
|
||||
title: self.$lang('submitSuccess'),
|
||||
title: '提交成功',
|
||||
icon: 'success'
|
||||
});
|
||||
// 重置表单
|
||||
@@ -345,7 +310,7 @@ export default {
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: res.message || self.$lang('submitFail'),
|
||||
title: res.message || '提交失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -159,10 +159,7 @@
|
||||
<!-- 商品推荐 -->
|
||||
<diy-goods-recommend :value="item"></diy-goods-recommend>
|
||||
</template>
|
||||
<template v-if="item.componentName == 'DiyRecommendCode'">
|
||||
<!-- 自定义推荐码组件 -->
|
||||
<diy-recommend-code :value="item"></diy-recommend-code>
|
||||
</template>
|
||||
|
||||
<template v-if="item.componentName == 'GoodsBrand'">
|
||||
<!-- 商品品牌 -->
|
||||
<diy-goods-brand :value="item"></diy-goods-brand>
|
||||
@@ -309,11 +306,9 @@ export default {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
console.log('[DIY-GROUP][created] hasDiyData=', !!this.diyData, 'valueLen=', this.diyData && this.diyData.value && this.diyData.value.length);
|
||||
this.processData();
|
||||
},
|
||||
mounted() {
|
||||
console.log('[DIY-GROUP][mounted] hasDiyData=', !!this.diyData, 'valueLen=', this.diyData && this.diyData.value && this.diyData.value.length);
|
||||
if (this.diyData && this.diyData.value) {
|
||||
this._notifyLoaded();
|
||||
}
|
||||
@@ -354,13 +349,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
_notifyLoaded() {
|
||||
console.log('[DIY-GROUP][_notifyLoaded] enter, will emit loaded in 300ms');
|
||||
if (this._loadedTimer) clearTimeout(this._loadedTimer);
|
||||
this._loadedTimer = setTimeout(() => {
|
||||
console.log('[DIY-GROUP][_notifyLoaded] emit loaded');
|
||||
this.$emit('loaded');
|
||||
}, 300);
|
||||
},
|
||||
if (this._loadedTimer) clearTimeout(this._loadedTimer);
|
||||
this._loadedTimer = setTimeout(() => {
|
||||
this.$emit('loaded');
|
||||
}, 300);
|
||||
},
|
||||
processData() {
|
||||
if (!this.diyGlobalData || !this.diyGlobalData.value) {
|
||||
this.processedData = [];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,21 +54,17 @@
|
||||
<!-- 视频播放弹窗 -->
|
||||
<uni-popup ref="videoPopup" type="center" style="background:transparent;width:100%;height:100%;">
|
||||
<view class="video-container" :style="popupVideoStyle">
|
||||
<video
|
||||
:key="video_url"
|
||||
<video
|
||||
ref="popupVideoRef"
|
||||
class="adaptive-video"
|
||||
:autoPauseIfNavigate="true"
|
||||
class="adaptive-video"
|
||||
:autoPauseIfNavigate="true"
|
||||
:autoPauseIfOpenNative="true"
|
||||
:autoplay="false"
|
||||
:enableAutoRotation="true"
|
||||
id="myVideo"
|
||||
:src="video_url"
|
||||
:autoplay="false"
|
||||
:enableAutoRotation="true"
|
||||
id="myVideo"
|
||||
:src="video_url"
|
||||
:controls="true"
|
||||
object-fit="contain"
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
x5-playsinline
|
||||
@loadedmetadata="onPopupVideoLoaded"
|
||||
@error="onPopupVideoError"
|
||||
></video>
|
||||
@@ -148,21 +144,41 @@ export default {
|
||||
|
||||
// 弹窗视频容器样式
|
||||
popupVideoStyle() {
|
||||
// 容器横向铺满,黑色背景填满左右边界
|
||||
// 视频用 object-fit:contain 保持原比例居中,左右黑边由容器黑色背景填充
|
||||
let style = 'position:fixed;width:100%;height:50vh;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:0;';
|
||||
let style = 'position:fixed;width:100%;left:0;';
|
||||
const defaultRatio = 9 / 16;
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 显示视频播放弹窗
|
||||
showVideo(item) {
|
||||
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.video_url = item.videoUrl;
|
||||
this.popupVideoWidth = 0;
|
||||
this.popupVideoHeight = 0;
|
||||
this.$refs.videoPopup.open();
|
||||
@@ -609,6 +625,22 @@ export default {
|
||||
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 {
|
||||
padding: 16rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
:style="videoWarpCss"
|
||||
controls
|
||||
preload="metadata"
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
x5-playsinline
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
@click="handlerClick(videoUrl)"
|
||||
></video>
|
||||
@@ -109,11 +106,11 @@ export default {
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
style += 'height:0;padding-top: ' + (ratio * 100) + '%;';
|
||||
style += 'padding-top: ' + (ratio * 100) + '%;';
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
style += 'height:0;padding-top: ' + (ratio * 100) + '%;';
|
||||
style += 'padding-top: ' + (ratio * 100) + '%;';
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-HARMONY
|
||||
|
||||
@@ -42,92 +42,82 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.loading-layer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 997;
|
||||
background: #f8f8f8;
|
||||
// 修复: 强制合成层, 让 spin 动画只在自身合成层跑, 不触发兄弟节点 (尤其是底部 TabBar) 重绘
|
||||
// 兼容环境: 微信小程序 / 鸿蒙元服务 / H5 都受益
|
||||
// - 微信小程序: spin 动画每帧导致兄弟 fixed 元素 TabBar 重绘
|
||||
// - 鸿蒙元服务 JSVM: 同上, JSVM 重绘更明显
|
||||
// - H5: will-change 提示浏览器持续合成
|
||||
transform: translateZ(0);
|
||||
-webkit-transform: translateZ(0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.loading-anim {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 40%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.loading-anim > .item {
|
||||
position: relative;
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
perspective: 1600rpx;
|
||||
transform-style: preserve-3d;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
.loading-anim .border {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 6rpx solid;
|
||||
}
|
||||
|
||||
.loading-anim .out {
|
||||
top: 15%;
|
||||
left: 15%;
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
// P0: 硬编码主色 → var(--base-color),主题切换时 loading 主色跟着变
|
||||
// 兼容: 微信小程序 / 鸿蒙元服务 — border-color 接受 var() 值无问题
|
||||
// JS 端 :style 绑定已用 themeStyle.main_color 走 inline 风格(原行 9),CSS 端用 var() 双保险
|
||||
border-left-color: var(--base-color, #FF4646);
|
||||
border-right-color: #C5C5C5 !important;
|
||||
// border-right-color: rgba($color: #000000, $alpha: 0) !important;
|
||||
border-top-color: var(--base-color, #FF4646);
|
||||
border-bottom-color: #C5C5C5 !important;
|
||||
// border-bottom-color: rgba($color: #000000, $alpha: 0) !important;
|
||||
animation: spin 0.6s linear normal infinite;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.loading-anim .in {
|
||||
top: 25%;
|
||||
left: 25%;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
border-top-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
animation: spin 0.8s linear infinite;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.loading-anim .mid {
|
||||
top: 40%;
|
||||
left: 40%;
|
||||
width: 20%;
|
||||
height: 20%;
|
||||
border-left-color: transparent;
|
||||
animation: spin 0.6s linear infinite;
|
||||
transform-origin: center center;
|
||||
}
|
||||
<style lang="scss">
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.loading-layer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 997;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.loading-anim {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 40%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.loading-anim > .item {
|
||||
position: relative;
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
perspective: 1600rpx;
|
||||
transform-style: preserve-3d;
|
||||
transition: all 0.2s ease-out;
|
||||
}
|
||||
|
||||
.loading-anim .border {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 6rpx solid;
|
||||
}
|
||||
|
||||
.loading-anim .out {
|
||||
top: 15%;
|
||||
left: 15%;
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
// P0: 硬编码主色 → var(--base-color),主题切换时 loading 主色跟着变
|
||||
// 兼容: 微信小程序 / 鸿蒙元服务 — border-color 接受 var() 值无问题
|
||||
// JS 端 :style 绑定已用 themeStyle.main_color 走 inline 风格(原行 9),CSS 端用 var() 双保险
|
||||
border-left-color: var(--base-color, #FF4646);
|
||||
border-right-color: #C5C5C5 !important;
|
||||
// border-right-color: rgba($color: #000000, $alpha: 0) !important;
|
||||
border-top-color: var(--base-color, #FF4646);
|
||||
border-bottom-color: #C5C5C5 !important;
|
||||
// border-bottom-color: rgba($color: #000000, $alpha: 0) !important;
|
||||
animation: spin 0.6s linear normal infinite;
|
||||
}
|
||||
|
||||
.loading-anim .in {
|
||||
top: 25%;
|
||||
left: 25%;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
border-top-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.loading-anim .mid {
|
||||
top: 40%;
|
||||
left: 40%;
|
||||
width: 20%;
|
||||
height: 20%;
|
||||
border-left-color: transparent;
|
||||
border-right-color: transparent;
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="form-wrap form-component" :style="formWrapStyle">
|
||||
<block v-for="(item, index) in formData" :key="index">
|
||||
<block v-for="(item, index) in formData">
|
||||
<view class="order-wrap" :class="rowWrapClass" :style="rowWrapStyle" v-if="item.controller == 'Text'">
|
||||
<view class="order-cell" :style="rowCellStyle">
|
||||
<view class="name" :style="nameWidthStyle">
|
||||
@@ -26,17 +26,24 @@
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box">
|
||||
<!-- 所有平台统一通过 formConfig.textareaRows / item.value.rows 控制显示行数:
|
||||
- H5 / 微信:原生 textarea 识别 :rows 属性,可视高度 = rows × line-height(CSS 控制 line-height);
|
||||
- 鸿蒙:渲染为原生 ArkUI TextArea,CSS height 不生效,必须靠 :rows 控制。
|
||||
行数和行高全部由 formConfig 动态下发,不再硬编码 176rpx / row=2。 -->
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<!-- 鸿蒙元服务渲染为原生 ArkUI TextArea,必须用 :rows 控制初始显示行数;CSS height 在原生组件上不生效 -->
|
||||
<textarea
|
||||
:rows="item.value.rows || textareaRowsValue"
|
||||
:rows="item.value.rows || 2"
|
||||
:placeholder="item.value.placeholder"
|
||||
placeholder-class="placeholder color-tip"
|
||||
v-model="item.val"
|
||||
:style="textareaStyle"
|
||||
:style="inputStyle"
|
||||
></textarea>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-HARMONY -->
|
||||
<textarea
|
||||
:placeholder="item.value.placeholder"
|
||||
placeholder-class="placeholder color-tip"
|
||||
v-model="item.val"
|
||||
:style="inputStyle"
|
||||
></textarea>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -63,8 +70,8 @@
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view :class="['box', 'check-group-box', 'option-layout-' + (item.value.option_layout || 'wrap')]">
|
||||
<checkbox-group @change="checkboxChange($event, index)" :style="item._optionLayoutStyle">
|
||||
<view class="box check-group-box">
|
||||
<checkbox-group @change="checkboxChange($event, index)">
|
||||
<label v-for="(v, k) in item.option_lists" :key="k">
|
||||
<checkbox :value="v.value" :checked="v.checked" />
|
||||
<view class="checkbox">
|
||||
@@ -83,8 +90,8 @@
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view :class="['box', 'radio-group-box', 'option-layout-' + (item.value.option_layout || 'wrap')]">
|
||||
<radio-group @change="radioChange($event, index)" :style="item._optionLayoutStyle">
|
||||
<view class="box radio-group-box">
|
||||
<radio-group @change="radioChange($event, index)">
|
||||
<label v-for="(v, k) in item.option_lists" :key="k">
|
||||
<radio :value="v.value" :checked="item.val == v.value" />
|
||||
<view class="radio-box">
|
||||
@@ -274,22 +281,10 @@
|
||||
labelFixedWidth: 180,
|
||||
labelFontSize: 28,
|
||||
labelColor: '#1f2937',
|
||||
labelFontWeight: 600,
|
||||
labelAutoMaxWidth: 200,
|
||||
requiredColor: '#ef4444',
|
||||
checkedColor: '#ff4544',
|
||||
placeholderColor: '#909399',
|
||||
placeholderFontSize: 28,
|
||||
// textarea 显示行数 / 行高(与 ns-newform 对齐)
|
||||
// 改用 :rows + line-height 驱动高度:rows 给原生 <textarea>(H5/微信原生层/鸿蒙 ArkUI 都识别),
|
||||
// line-height 用于在 textareaStyle 里算出对应的 px 高度;不再写死 176rpx。
|
||||
textareaRows: 4,
|
||||
textareaLineHeight: 44,
|
||||
// 输入框视觉增强(与 ns-newform 对齐)
|
||||
inputBorderColor: '#e5e7eb',
|
||||
inputBgColor: '#f9fafb',
|
||||
inputFocusBorderColor: '#ff4544',
|
||||
inputFocusBgColor: '#ffffff',
|
||||
showRowDivider: true,
|
||||
rowDividerColor: '#f0f0f0'
|
||||
};
|
||||
@@ -313,17 +308,10 @@
|
||||
labelFixedWidth: pick('labelFixedWidth', 'label_fixed_width'),
|
||||
labelFontSize: pick('labelFontSize', 'label_font_size'),
|
||||
labelColor: pick('labelColor', 'label_color'),
|
||||
labelFontWeight: pick('labelFontWeight', 'label_font_weight'),
|
||||
requiredColor: pick('requiredColor', 'required_color'),
|
||||
checkedColor: pick('checkedColor', 'checked_color'),
|
||||
placeholderColor: pick('placeholderColor', 'placeholder_color'),
|
||||
placeholderFontSize: pick('placeholderFontSize', 'placeholder_font_size'),
|
||||
textareaRows: pick('textareaRows', 'textarea_rows'),
|
||||
textareaLineHeight: pick('textareaLineHeight', 'textarea_line_height'),
|
||||
inputBorderColor: pick('inputBorderColor', 'input_border_color'),
|
||||
inputBgColor: pick('inputBgColor', 'input_bg_color'),
|
||||
inputFocusBorderColor: pick('inputFocusBorderColor', 'input_focus_border_color'),
|
||||
inputFocusBgColor: pick('inputFocusBgColor', 'input_focus_bg_color'),
|
||||
showRowDivider: pick('showRowDivider', 'show_row_divider'),
|
||||
rowDividerColor: pick('rowDividerColor', 'row_divider_color')
|
||||
};
|
||||
@@ -375,18 +363,10 @@
|
||||
'--label-fixed-width': this.$util.rpx(cfg.labelFixedWidth),
|
||||
'--label-font-size': this.$util.rpx(num(cfg.labelFontSize, 28)),
|
||||
'--label-color': cfg.labelColor,
|
||||
'--label-font-weight': cfg.labelFontWeight,
|
||||
'--required-color': cfg.requiredColor,
|
||||
'--checked-color': cfg.checkedColor,
|
||||
'--placeholder-color': cfg.placeholderColor,
|
||||
'--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 对齐)
|
||||
'--input-border-color': cfg.inputBorderColor,
|
||||
'--input-bg-color': cfg.inputBgColor,
|
||||
'--input-focus-border-color': cfg.inputFocusBorderColor,
|
||||
'--input-focus-bg-color': cfg.inputFocusBgColor,
|
||||
'--row-divider-color': cfg.rowDividerColor,
|
||||
'--show-row-divider': cfg.showRowDivider === false ? '0' : '1'
|
||||
});
|
||||
@@ -422,42 +402,6 @@
|
||||
'line-height': this.$util.rpx(this.config.inputHeight)
|
||||
});
|
||||
},
|
||||
// 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),
|
||||
// 对 textarea 太小,会让原生 textarea 单行过高、可视区域过小;
|
||||
// 2) 不使用 auto-height:原生 textarea 上 auto-height 会让组件随内容无限扩展,
|
||||
// 遮挡后续组件,导致"看不到下面的其他组件";
|
||||
// 3) 高度 = rows × lineHeight(两个值都由 formConfig 动态下发,不再写死 176rpx):
|
||||
// - H5 浏览器 / 微信原生层:原生 <textarea> 同时识别 :rows 和 CSS line-height,
|
||||
// 高度由浏览器自然算出,与 :rows 完全一致;
|
||||
// - 鸿蒙 ArkUI:原生 TextArea 只识别 :rows,不识别 CSS height,所以我们在 JS 里
|
||||
// 显式算 height = rows × lineHeight 反向写出,保证三端可视高度像素级一致。
|
||||
textareaStyle() {
|
||||
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({
|
||||
'height': h,
|
||||
'min-height': h,
|
||||
'line-height': lh,
|
||||
'border-radius': this.$util.rpx(this.config.inputRadius),
|
||||
'font-size': this.$util.rpx(this.config.labelFontSize),
|
||||
'color': this.config.labelColor
|
||||
});
|
||||
},
|
||||
// 必填星号颜色
|
||||
requiredColor() {
|
||||
return this.config.requiredColor;
|
||||
@@ -487,33 +431,10 @@
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 预计算每项的 option_layout inline style(与 ns-newform 对齐)
|
||||
// 跨端兼容:小程序 / 鸿蒙元服务 WXML 不支持模板里的方法调用,必须预计算到 item 上
|
||||
recomputeItemStyles() {
|
||||
if (!this.formData || this.formData.length === 0) return;
|
||||
this.formData.forEach((item) => {
|
||||
const itemValue = (item && item.value) || {};
|
||||
const optLayout = itemValue.option_layout;
|
||||
const optStyle = {};
|
||||
if (optLayout === 'vertical') {
|
||||
optStyle['flex-direction'] = 'column';
|
||||
optStyle['align-items'] = 'flex-start';
|
||||
optStyle['gap'] = '8px';
|
||||
} else {
|
||||
// wrap 模式
|
||||
optStyle['flex-direction'] = 'row';
|
||||
optStyle['align-items'] = 'center';
|
||||
optStyle['gap'] = '6px';
|
||||
}
|
||||
this.$set(item, '_optionLayoutStyle', optStyle);
|
||||
});
|
||||
},
|
||||
initFormData() {
|
||||
// 深拷贝 prop 数据到本地状态,避免 mutate 上游
|
||||
this.formData = JSON.parse(JSON.stringify(this.data || []));
|
||||
this.processFormData();
|
||||
// 预计算每项 inline style(与 ns-newform 对齐,跨端兼容)
|
||||
this.recomputeItemStyles();
|
||||
},
|
||||
setFormData() {
|
||||
this.initFormData();
|
||||
@@ -521,9 +442,7 @@
|
||||
},
|
||||
processFormData() {
|
||||
this.formData.forEach(item => {
|
||||
// Text / Textarea:当 placeholder 为空字符串或没有值时,不设置默认值
|
||||
const skipDefault = (item.controller == 'Text' || item.controller == 'Textarea') && !item.value.placeholder;
|
||||
if (!item.val && !skipDefault) item.val = item.value.default ? item.value.default : '';
|
||||
if (!item.val) item.val = item.value.default ? item.value.default : '';
|
||||
|
||||
if (item.value.options) {
|
||||
item.option_lists = [];
|
||||
@@ -958,19 +877,10 @@
|
||||
--label-fixed-width: 180rpx;
|
||||
--label-font-size: 28rpx;
|
||||
--label-color: #1f2937;
|
||||
--label-font-weight: 600;
|
||||
--required-color: #ef4444;
|
||||
--placeholder-font-size: 28rpx;
|
||||
--checked-color: #ff4544;
|
||||
--placeholder-color: #909399;
|
||||
// textarea 行数 / 行高(运行时被 formWrapStyle 注入的 inline 覆盖;这里是兜底)
|
||||
--textarea-rows: 4;
|
||||
--textarea-line-height: 44rpx;
|
||||
// 输入框视觉增强(与 ns-newform 对齐)
|
||||
--input-border-color: #e5e7eb;
|
||||
--input-bg-color: #f9fafb;
|
||||
--input-focus-border-color: #ff4544;
|
||||
--input-focus-bg-color: #ffffff;
|
||||
--row-divider-color: #f0f0f0;
|
||||
--show-row-divider: 1;
|
||||
}
|
||||
@@ -1034,7 +944,6 @@
|
||||
text-align: left;
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
color: var(--label-color, #1f2937);
|
||||
font-weight: var(--label-font-weight, 600);
|
||||
|
||||
text {
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
@@ -1056,8 +965,6 @@
|
||||
line-height: inherit;
|
||||
text-align: left;
|
||||
|
||||
// H5 端 <input> 可能被 uni-app 编译为 <uni-input> 包装组件,::v-deep 穿透 scoped 命中真实原生 input
|
||||
::v-deep input,
|
||||
input {
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
text-align: left;
|
||||
@@ -1069,16 +976,8 @@
|
||||
min-height: var(--input-height, 60rpx);
|
||||
border-radius: var(--input-radius, 0rpx);
|
||||
box-sizing: border-box;
|
||||
padding: 0 12rpx;
|
||||
// 输入框视觉增强(与 ns-newform 对齐)
|
||||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||||
background: var(--input-bg-color, #f9fafb);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:focus {
|
||||
background: var(--input-focus-bg-color, #ffffff);
|
||||
border-color: var(--input-focus-border-color, #ff4544);
|
||||
}
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
|
||||
// 原生 placeholder 样式(WebView / H5 生效;小程序/鸿蒙的 placeholder
|
||||
// 通过 placeholder-class="placeholder" 钩子走 .placeholder 规则)
|
||||
@@ -1096,30 +995,20 @@
|
||||
line-height: var(--input-height, 60rpx);
|
||||
}
|
||||
|
||||
::v-deep textarea,
|
||||
textarea {
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
width: 100%;
|
||||
color: var(--label-color, #1f2937);
|
||||
// 行高由 formConfig.textareaLineHeight(兜底 44rpx)控制;
|
||||
// 高度 = rows × line-height + padding + border,所有值都从 CSS 变量读取,
|
||||
// 真正做到了"rows 改 → 高度自动改"。
|
||||
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);
|
||||
// 鸿蒙元服务渲染为原生 ArkUI TextArea,不识别 CSS height,由 :rows 控制行数;
|
||||
// 这里给所有端都设一个最小高度基线,避免空内容时塌缩
|
||||
min-height: 88rpx;
|
||||
height: 88rpx;
|
||||
line-height: 44rpx;
|
||||
text-align: left;
|
||||
border-radius: var(--input-radius, 0rpx);
|
||||
box-sizing: border-box;
|
||||
padding: 12rpx;
|
||||
// 输入框视觉增强(与 ns-newform 对齐)
|
||||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||||
background: var(--input-bg-color, #f9fafb);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:focus {
|
||||
background: var(--input-focus-bg-color, #ffffff);
|
||||
border-color: var(--input-focus-border-color, #ff4544);
|
||||
}
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.uni-input {
|
||||
@@ -1127,11 +1016,6 @@
|
||||
color: var(--label-color, #1f2937);
|
||||
line-height: var(--input-height, 60rpx);
|
||||
min-height: var(--input-height, 60rpx);
|
||||
// 输入框视觉增强(与 ns-newform 对齐)
|
||||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||||
background: var(--input-bg-color, #f9fafb);
|
||||
border-radius: var(--input-radius, 0rpx);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.select-address {
|
||||
@@ -1153,15 +1037,6 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
// 选项排列样式:vertical = 每个选项独立一行;wrap(默认)= 横向自动换行
|
||||
.option-layout-vertical {
|
||||
checkbox-group,
|
||||
radio-group {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
return {
|
||||
list: [],
|
||||
config: {
|
||||
title: '',
|
||||
title: '猜你喜欢',
|
||||
sources: 'sort',
|
||||
supportPage: [],
|
||||
goodsIds: [],
|
||||
fontWeight: false,
|
||||
padding: 0,
|
||||
cartEvent: 'detail',
|
||||
text: '',
|
||||
text: '购买',
|
||||
textColor: '#FFFFFF',
|
||||
theme: 'default',
|
||||
aroundRadius: 25,
|
||||
@@ -67,13 +67,10 @@
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// 初始化 i18n 默认文案,实际展示以服务端配置 config.title / config.text 为准
|
||||
if (!this.config.title) this.config.title = this.$lang('common.goodsRecommendTitle');
|
||||
if (!this.config.text) this.config.text = this.$lang('buyNow');
|
||||
if (this.auto) {
|
||||
this.getLikeList();
|
||||
}
|
||||
},
|
||||
if (this.auto) {
|
||||
this.getLikeList();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.list = [];
|
||||
|
||||
@@ -56,18 +56,10 @@
|
||||
|
||||
<!-- 手机号授权按钮 -->
|
||||
<block v-if="isAgree">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button class="auth-login save-btn border-0" style="border: none;" open-type="getPhoneNumber"
|
||||
@getphonenumber="getPhoneNumber">
|
||||
<text style="color: #fff; font-size: 30rpx;">{{ $lang('components.ns-login.agreeAndAuthorizeMobile') }}</text>
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<!-- 鸿蒙元服务无 getPhoneNumber 微信 API,改为弹出手机号+验证码表单 -->
|
||||
<button class="auth-login save-btn border-0" style="border: none;" @tap="showHarmonyMobileForm = true">
|
||||
<text style="color: #fff; font-size: 30rpx;">{{ $lang('components.ns-login.agreeAndAuthorizeMobile') }}</text>
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
</block>
|
||||
<block v-else>
|
||||
<button class="auth-login save-btn border-0" style="border: none;" @tap="authno">
|
||||
@@ -75,27 +67,6 @@
|
||||
</button>
|
||||
</block>
|
||||
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<!-- 鸿蒙专属:手机号+验证码表单 -->
|
||||
<view v-if="showHarmonyMobileForm" class="harmony-mobile-form" @touchmove.prevent.stop>
|
||||
<view class="item-wrap">
|
||||
<text class="label">{{ $lang('components.ns-login.mobileLabel') }}</text>
|
||||
<input type="number" :placeholder="$lang('components.ns-login.mobilePlaceholder')" v-model="formData.mobile" maxlength="11" />
|
||||
</view>
|
||||
<view class="item-wrap" v-if="isOpenCaptcha == 1">
|
||||
<text class="label">{{ $lang('components.ns-login.captchaLabel') }}</text>
|
||||
<input type="number" :placeholder="$lang('components.ns-login.captchaPlaceholder')" v-model="formData.vercode" maxlength="4" />
|
||||
<image :src="captcha.img" class="captcha" @tap="getCaptcha" show-menu-by-longpress></image>
|
||||
</view>
|
||||
<view class="item-wrap">
|
||||
<text class="label">{{ $lang('components.ns-login.dynacodeLabel') }}</text>
|
||||
<input type="number" :placeholder="$lang('components.ns-login.dynacodePlaceholder')" v-model="formData.dynacode" maxlength="4" />
|
||||
<view class="send color-base-text" @tap="sendMobileCode">{{ dynacodeData.codeText }}</view>
|
||||
</view>
|
||||
<button type="default" class="save-btn" @tap="harmonySaveMobile" :disabled="isDisabled">{{ $lang('components.ns-login.save') }}</button>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<view @tap="closeLogin"
|
||||
style="font-size: 30rpx; color: #888; text-align: center; padding-top: 10rpx;">
|
||||
{{ $lang('components.ns-login.notNow') }}
|
||||
@@ -138,20 +109,11 @@
|
||||
|
||||
<view class="item-wrap" v-if="forceBindingMobileControl">
|
||||
<text class="label">{{ $lang('components.ns-login.mobileLabel') }}</text>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<button open-type="getPhoneNumber" :plain="true" class="auth-login border-0"
|
||||
@getphonenumber="getPhoneNumber">
|
||||
<text class="mobile" v-if="formData.mobile">{{ formData.mobile }}</text>
|
||||
<text class="color-base-text" v-else>{{ $lang('components.ns-login.getMobile') }}</text>
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<!-- 鸿蒙无 getPhoneNumber,展示手动输入的手机号,没有时显示"获取手机号"提示用户去上面表单录入 -->
|
||||
<view class="auth-login border-0" :plain="true">
|
||||
<text class="mobile" v-if="formData.mobile">{{ formData.mobile }}</text>
|
||||
<text class="color-base-text" v-else @tap="showHarmonyMobileForm = true">{{ $lang('components.ns-login.getMobile') }}</text>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<button type="default" class="save-btn" @tap="saveMp" :disabled="isDisabled">{{ $lang('components.ns-login.save') }}</button>
|
||||
</template>
|
||||
@@ -208,16 +170,12 @@ export default {
|
||||
isSend: false
|
||||
},
|
||||
// 小程序获取手机号所需数据
|
||||
authMobileData: {
|
||||
iv: '',
|
||||
encryptedData: ''
|
||||
},
|
||||
site_name: '',
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙专属:控制手机号+验证码表单显示
|
||||
showHarmonyMobileForm: false,
|
||||
// #endif
|
||||
};
|
||||
authMobileData: {
|
||||
iv: '',
|
||||
encryptedData: ''
|
||||
},
|
||||
site_name: ''
|
||||
};
|
||||
},
|
||||
options: {
|
||||
styleIsolation: 'shared'
|
||||
@@ -245,7 +203,7 @@ export default {
|
||||
if (this.forceBindingMobileControl) {
|
||||
if (this.formData.mobile.length == 0) return true;
|
||||
|
||||
// #ifdef H5 || MP-HARMONY
|
||||
// #ifdef H5
|
||||
// 验证码
|
||||
if (this.isOpenCaptcha == 1 && this.formData.vercode.length == 0) return true;
|
||||
// 动态码
|
||||
@@ -385,7 +343,7 @@ export default {
|
||||
openCompleteInfoPop() {
|
||||
this.getRegisterConfig();
|
||||
|
||||
// #ifdef H5 || MP-HARMONY
|
||||
// #ifdef H5
|
||||
if (!this.storeToken) this.getCaptchaConfig();
|
||||
// #endif
|
||||
|
||||
@@ -718,26 +676,6 @@ export default {
|
||||
this.forceBindMobile();
|
||||
},
|
||||
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙元服务保存手机号:复用 H5 的强制绑定手机号流程
|
||||
harmonySaveMobile() {
|
||||
if (this.forceBindingMobileControl) {
|
||||
let data = {
|
||||
mobile: this.formData.mobile,
|
||||
key: this.formData.key,
|
||||
code: this.formData.dynacode
|
||||
};
|
||||
if (this.captcha.id != '') {
|
||||
data.captcha_id = this.captcha.id;
|
||||
data.captcha_code = this.formData.vercode;
|
||||
}
|
||||
if (!this.verify(data)) return;
|
||||
}
|
||||
// 鸿蒙环境下通常也开启了 forceBindingMobileControl(走 forceBindMobile 接口)
|
||||
this.forceBindMobile();
|
||||
},
|
||||
// #endif
|
||||
|
||||
// 微信小程序保存数据
|
||||
saveMp() {
|
||||
if (this.formData.mobile === '') {
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
let systemInfo = uni.getSystemInfoSync();
|
||||
let menuButtonInfo = {};
|
||||
// 如果是小程序,获取右上角胶囊的尺寸信息,避免导航栏右侧内容与胶囊重叠(支付宝小程序非本API,尚未兼容)
|
||||
// #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-HARMONY
|
||||
// #ifdef MP-WEIXIN || MP-BAIDU || MP-TOUTIAO || MP-QQ
|
||||
menuButtonInfo = uni.getMenuButtonBoundingClientRect();
|
||||
// #endif
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view :class="['box', 'align-left']" :style="item._boxAlignStyle">
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="item.value.placeholder"
|
||||
placeholder-class="placeholder color-tip"
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="item.value.placeholder || $lang('components.ns-newform.inputPlaceholder')"
|
||||
placeholder-class="placeholder color-tip"
|
||||
v-model="item.val"
|
||||
/>
|
||||
</view>
|
||||
@@ -27,16 +27,10 @@
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<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
|
||||
:rows="item.value.rows || textareaRowsValue"
|
||||
:placeholder="item.value.placeholder"
|
||||
placeholder-class="placeholder color-tip"
|
||||
<textarea
|
||||
:placeholder="item.value.placeholder || $lang('components.ns-newform.inputPlaceholder')"
|
||||
placeholder-class="placeholder color-tip"
|
||||
v-model="item.val"
|
||||
:style="textareaStyle"
|
||||
></textarea>
|
||||
</view>
|
||||
</view>
|
||||
@@ -70,8 +64,8 @@
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view :class="['box', 'check-group-box', 'align-left', 'option-layout-' + (item.value.option_layout || 'wrap')]">
|
||||
<checkbox-group @change="checkboxChange($event, index)" :style="item._optionLayoutStyle">
|
||||
<view :class="['box', 'check-group-box', 'align-left']">
|
||||
<checkbox-group @change="checkboxChange($event, index)">
|
||||
<label v-for="(v, k) in item.option_lists" :key="k">
|
||||
<checkbox :value="v.value" :checked="v.checked"></checkbox>
|
||||
<view class="checkbox">
|
||||
@@ -98,8 +92,8 @@
|
||||
<text class="tit">{{ item.value.title }}</text>
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view :class="['box', 'radio-group-box', 'align-left', 'option-layout-' + (item.value.option_layout || 'wrap')]">
|
||||
<radio-group @change="radioChange($event, index)" :style="item._optionLayoutStyle">
|
||||
<view :class="['box', 'radio-group-box', 'align-left']">
|
||||
<radio-group @change="radioChange($event, index)">
|
||||
<label v-for="(v, k) in item.option_lists" :key="k">
|
||||
<radio :value="v.value" :checked="item.val == v.value"></radio>
|
||||
<view class="radio-box">
|
||||
@@ -377,22 +371,10 @@ export default {
|
||||
labelFixedWidth: 180,
|
||||
labelFontSize: 28,
|
||||
labelColor: '#1f2937',
|
||||
labelFontWeight: 600,
|
||||
labelAutoMaxWidth: 200, // auto 模式下 label 宽度上限(rpx),超过后 ellipsis 截断
|
||||
requiredColor: '#ef4444',
|
||||
checkedColor: '#ff4544',
|
||||
placeholderColor: '#ddd',
|
||||
placeholderFontSize: 28,
|
||||
// textarea 显示行数 / 行高(与 ns-form 保持一致,由 formConfig 覆盖)
|
||||
// 改用 :rows + line-height 驱动高度,rows 给原生 <textarea>,line-height 在 textareaStyle 算 px 高度
|
||||
textareaRows: 4,
|
||||
textareaLineHeight: 44,
|
||||
// 输入框视觉增强(可由后端 form_config 覆盖)
|
||||
inputBorderColor: '#e5e7eb',
|
||||
inputBgColor: '#f9fafb',
|
||||
inputFocusBorderColor: '#ff4544',
|
||||
inputFocusBgColor: '#ffffff',
|
||||
labelFontWeight: 600,
|
||||
// 提交按钮
|
||||
submitColor: '#ffffff',
|
||||
submitBgColor: '#ff4544',
|
||||
@@ -462,15 +444,6 @@ export default {
|
||||
'--checked-color': cfg.checkedColor,
|
||||
'--placeholder-color': cfg.placeholderColor,
|
||||
'--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 字重)
|
||||
'--input-border-color': cfg.inputBorderColor,
|
||||
'--input-bg-color': cfg.inputBgColor,
|
||||
'--input-focus-border-color': cfg.inputFocusBorderColor,
|
||||
'--input-focus-bg-color': cfg.inputFocusBgColor,
|
||||
'--label-font-weight': cfg.labelFontWeight,
|
||||
'--submit-color': cfg.submitColor,
|
||||
'--submit-bg-color': cfg.submitBgColor,
|
||||
'--submit-margin-top': this.$util.rpx(cfg.submitMarginTop || 0),
|
||||
@@ -510,30 +483,6 @@ export default {
|
||||
'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() {
|
||||
const style = {
|
||||
fontSize: this.$util.rpx(this.config.labelFontSize),
|
||||
@@ -587,23 +536,6 @@ export default {
|
||||
// item 级别 box 对齐方式(与原 getItemBoxAlignStyle 逻辑一致)
|
||||
const align = itemValue.input_align;
|
||||
this.$set(item, '_boxAlignStyle', align ? { 'text-align': align } : {});
|
||||
|
||||
// 选项排列样式:vertical 时给 checkbox-group / radio-group 加 flex-direction
|
||||
// 走 inline style(不依赖 dev 编译产物的 SCSS 类,跨端兼容更稳)
|
||||
// 直接写 flex-direction 等 CSS 属性,inline style 优先级最高
|
||||
const optLayout = itemValue.option_layout;
|
||||
const optStyle = {};
|
||||
if (optLayout === 'vertical') {
|
||||
optStyle['flex-direction'] = 'column';
|
||||
optStyle['align-items'] = 'flex-start';
|
||||
optStyle['gap'] = '8px';
|
||||
} else {
|
||||
// wrap 模式:用 display: flex 基础属性兜底(不强制,让浏览器按 SCSS 默认渲染)
|
||||
optStyle['flex-direction'] = 'row';
|
||||
optStyle['align-items'] = 'center';
|
||||
optStyle['gap'] = '6px';
|
||||
}
|
||||
this.$set(item, '_optionLayoutStyle', optStyle);
|
||||
});
|
||||
},
|
||||
// 容器样式(提供给外部 diy-form 使用)
|
||||
@@ -627,13 +559,8 @@ export default {
|
||||
if (len > maxChars) maxChars = len;
|
||||
});
|
||||
// 与后端公式一致:chars * (fontSize/2) * 1.1 + 12,单位 px → 转 rpx(×2)
|
||||
let widthPx = Math.max(40, maxChars * (fontSize / 2) * 1.1 + 12);
|
||||
let widthRpx = Math.round(widthPx * 2);
|
||||
// auto 模式下加宽度上限:超长 title 撑爆布局时,按 labelAutoMaxWidth 截断(rpx)
|
||||
// 上限内 ellipsis 截断,保证控件区有足够空间;上限外也不会让 label 占满整行
|
||||
const maxWidthRpx = parseInt(this.config.labelAutoMaxWidth) || 200;
|
||||
if (widthRpx > maxWidthRpx) widthRpx = maxWidthRpx;
|
||||
this.maxLabelWidth = widthRpx;
|
||||
const widthPx = Math.max(40, maxChars * (fontSize / 2) * 1.1 + 12);
|
||||
this.maxLabelWidth = Math.round(widthPx * 2);
|
||||
// 宽度变了,需要把全局 nameWidthStyle 重新合到每项 _labelStyle 里
|
||||
this.recomputeItemStyles();
|
||||
},
|
||||
@@ -651,9 +578,7 @@ export default {
|
||||
|
||||
processFormData() {
|
||||
this.formData.forEach((item) => {
|
||||
// Text / Textarea:当 placeholder 为空字符串或没有值时,不设置默认值
|
||||
const skipDefault = (item.controller == 'Text' || item.controller == 'Textarea') && !item.value.placeholder;
|
||||
if (!item.val && !skipDefault) {
|
||||
if (!item.val) {
|
||||
item.val = item.value.default ? item.value.default : '';
|
||||
}
|
||||
|
||||
@@ -1034,15 +959,6 @@ export default {
|
||||
--submit-bg-color: #ff4544;
|
||||
--placeholder-color: #ddd;
|
||||
--placeholder-font-size: 28rpx;
|
||||
// textarea 行数 / 行高(运行时被 formWrapStyle 注入的 inline 覆盖;这里是兜底)
|
||||
--textarea-rows: 4;
|
||||
--textarea-line-height: 44rpx;
|
||||
// 输入框视觉增强(边框 / 背景 / 聚焦 / label 字重)
|
||||
--input-border-color: #e5e7eb;
|
||||
--input-bg-color: #f9fafb;
|
||||
--input-focus-border-color: #ff4544;
|
||||
--input-focus-bg-color: #ffffff;
|
||||
--label-font-weight: 600;
|
||||
--show-row-divider: 1;
|
||||
|
||||
// 与后端 form-preview-wrap.no-divider-all 对齐:整组隐藏所有行分割线
|
||||
@@ -1094,13 +1010,13 @@ export default {
|
||||
&.vertical-layout {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
|
||||
.name {
|
||||
width: 100%;
|
||||
margin-bottom: var(--label-gap, 16rpx);
|
||||
margin-bottom: 8rpx;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
|
||||
.box {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1150,7 +1066,7 @@ export default {
|
||||
text-align: left;
|
||||
font-size: var(--item-label-font-size, var(--label-font-size, 28rpx));
|
||||
color: var(--item-label-color, var(--label-color, #1f2937));
|
||||
font-weight: var(--item-label-font-weight, var(--label-font-weight, 600));
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.2rpx;
|
||||
|
||||
text {
|
||||
@@ -1218,107 +1134,49 @@ export default {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// H5 端 <input> 被 uni-app 编译为 <uni-input> 自定义组件,
|
||||
// 真实 <input> 是其子节点,需要 ::v-deep 穿透 scoped 才能命中
|
||||
::v-deep input {
|
||||
font-size: var(--label-font-size, 28rpx);
|
||||
text-align: left;
|
||||
height: var(--input-height, 60rpx);
|
||||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||||
border-radius: var(--input-radius, 10rpx);
|
||||
line-height: var(--input-height, 60rpx);
|
||||
padding: 0 12rpx;
|
||||
background: var(--input-bg-color, #f9fafb);
|
||||
color: var(--label-color, #1f2937);
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
background: var(--input-focus-bg-color, #ffffff);
|
||||
border-color: var(--input-focus-border-color, #ff4544);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--placeholder-color, #ddd);
|
||||
font-size: var(--placeholder-font-size, 28rpx);
|
||||
}
|
||||
}
|
||||
|
||||
// 非 H5 端(小程序 / 鸿蒙 / App)<input> 是直接原生元素,普通选择器即可
|
||||
input {
|
||||
font-size: var(--label-font-size, 28rpx);
|
||||
text-align: left;
|
||||
height: var(--input-height, 60rpx);
|
||||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||||
border: none;
|
||||
border-radius: var(--input-radius, 10rpx);
|
||||
line-height: var(--input-height, 60rpx);
|
||||
padding: 0 12rpx;
|
||||
background: var(--input-bg-color, #f9fafb);
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--label-color, #1f2937);
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
&:focus {
|
||||
background: var(--input-focus-bg-color, #ffffff);
|
||||
border-color: var(--input-focus-border-color, #ff4544);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
&::placeholder {
|
||||
color: var(--placeholder-color, #ddd);
|
||||
font-size: var(--placeholder-font-size, 28rpx);
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep textarea {
|
||||
font-size: var(--label-font-size, 28rpx);
|
||||
width: 100%;
|
||||
// 行高 / 高度全部由 formConfig 动态决定(CSS 兜底公式与 JS textareaStyle 一致)
|
||||
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;
|
||||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||||
border-radius: var(--input-radius, 10rpx);
|
||||
padding: 12rpx;
|
||||
background: var(--input-bg-color, #f9fafb);
|
||||
color: var(--label-color, #1f2937);
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
background: var(--input-focus-bg-color, #ffffff);
|
||||
border-color: var(--input-focus-border-color, #ff4544);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--placeholder-color, #ddd);
|
||||
font-size: var(--placeholder-font-size, 28rpx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
textarea {
|
||||
font-size: var(--label-font-size, 28rpx);
|
||||
width: 100%;
|
||||
// 行高 / 高度全部由 formConfig 动态决定(与 ::v-deep textarea 同步)
|
||||
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);
|
||||
min-height: 120rpx;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||||
border: none;
|
||||
border-radius: var(--input-radius, 10rpx);
|
||||
padding: 12rpx;
|
||||
background: var(--input-bg-color, #f9fafb);
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--label-color, #1f2937);
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
|
||||
height: auto;
|
||||
|
||||
&:focus {
|
||||
background: var(--input-focus-bg-color, #ffffff);
|
||||
border-color: var(--input-focus-border-color, #ff4544);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
&::placeholder {
|
||||
color: var(--placeholder-color, #ddd);
|
||||
font-size: var(--placeholder-font-size, 28rpx);
|
||||
@@ -1381,16 +1239,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 选项排列样式:vertical = 每个选项独立一行;wrap(默认)= 横向自动换行
|
||||
.option-layout-vertical {
|
||||
checkbox-group,
|
||||
radio-group {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -70,11 +70,37 @@ export default {
|
||||
return {
|
||||
isIphoneX: false,
|
||||
payIndex: 0,
|
||||
payTypeList: [],
|
||||
// #ifdef H5
|
||||
payTypeList: [{
|
||||
name: this.$lang('components.nsPayment.alipay'),
|
||||
icon: 'icon-zhifubaozhifu-',
|
||||
type: 'alipay'
|
||||
},
|
||||
{
|
||||
name: this.$lang('components.nsPayment.wechatpay'),
|
||||
icon: 'icon-weixin1',
|
||||
type: 'wechatpay'
|
||||
}
|
||||
],
|
||||
timer: null,
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
payTypeList: [{
|
||||
name: this.$lang('components.nsPayment.wechatpay'),
|
||||
provider: 'wxpay',
|
||||
icon: 'icon-weixin1',
|
||||
type: 'wechatpay'
|
||||
}],
|
||||
isMatched: 0, // 支付是否需要校验
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
payTypeList: [{
|
||||
name: this.$lang('components.nsPayment.alipay'),
|
||||
icon: 'icon-zhifubaozhifu-',
|
||||
type: 'alipay',
|
||||
provider: 'alipay'
|
||||
}],
|
||||
// #endif
|
||||
payInfo: {},
|
||||
balanceConfig: 0,
|
||||
// 预售页面判断
|
||||
@@ -82,30 +108,6 @@ export default {
|
||||
};
|
||||
},
|
||||
created(e) {
|
||||
// #ifdef H5
|
||||
this.payTypeList = [
|
||||
{ name: this.$lang('components.nsPayment.alipay'), icon: 'icon-zhifubaozhifu-', type: 'alipay' },
|
||||
{ name: this.$lang('components.nsPayment.wechatpay'), icon: 'icon-weixin1', type: 'wechatpay' },
|
||||
];
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
this.payTypeList = [
|
||||
{ name: this.$lang('components.nsPayment.wechatpay'), provider: 'wxpay', icon: 'icon-weixin1', type: 'wechatpay' },
|
||||
];
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
this.payTypeList = [
|
||||
{ name: this.$lang('components.nsPayment.alipay'), icon: 'icon-zhifubaozhifu-', type: 'alipay', provider: 'alipay' },
|
||||
];
|
||||
// #endif
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙元服务的支付通道: 与微信小程序相同的 provider='wxpay',
|
||||
// 但走鸿蒙 JSSDK 的 requestPayment(走鸿蒙支付服务而非 wx.requestPayment)
|
||||
this.payTypeList = [
|
||||
{ name: this.$lang('components.nsPayment.wechatpay'), provider: 'wxpay', icon: 'icon-weixin1', type: 'wechatpay' },
|
||||
];
|
||||
// #endif
|
||||
|
||||
let url;
|
||||
// #ifdef H5
|
||||
url = window.location.href
|
||||
|
||||
@@ -9,13 +9,9 @@
|
||||
<view class="video-popup-body">
|
||||
<video
|
||||
v-if="videoUrl"
|
||||
id="popupVideoPlayer"
|
||||
:src="videoUrl"
|
||||
controls
|
||||
autoplay
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
x5-playsinline
|
||||
class="video-player"
|
||||
></video>
|
||||
<view v-else class="video-error">{{ $lang('components.ns-video-player-popup.videoUrlNotExist') }}</view>
|
||||
@@ -26,8 +22,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { playVideoInContext } from '@/common/js/video-helper.js';
|
||||
|
||||
export default {
|
||||
name: 'ns-video-player-popup',
|
||||
props: {
|
||||
@@ -82,10 +76,6 @@ export default {
|
||||
* @param {Object} e - 弹窗状态变化事件
|
||||
*/
|
||||
onVideoPopupChange(e) {
|
||||
// H5 端弹窗显示后触发播放:避免 autoplay 被浏览器拦截 / 弹窗动画未完成被 pause() 打断
|
||||
if (e && e.show) {
|
||||
playVideoInContext('popupVideoPlayer', this);
|
||||
}
|
||||
this.$emit('popup-change', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,16 +95,6 @@
|
||||
i18nKey: 'components.payment.alipay'
|
||||
}],
|
||||
// #endif
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙元服务的支付通道: provider='wxpay' 由鸿蒙 JSSDK 接管
|
||||
payTypeList: [{
|
||||
name: '微信支付',
|
||||
provider: 'wxpay',
|
||||
icon: 'icon-weixin1',
|
||||
type: 'wechatpay',
|
||||
i18nKey: 'components.payment.wechatpay'
|
||||
}],
|
||||
// #endif
|
||||
payInfo: null,
|
||||
balanceConfig: 0,
|
||||
// 预售页面判断
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
<template>
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<!--
|
||||
鸿蒙元服务已接入平台隐私托管, 由鸿蒙系统自动弹出系统级隐私弹窗,
|
||||
应用层不再渲染自建 privacy-popup (避免出现"多次弹出隐私弹窗"被驳回)。
|
||||
此处保留空模板, 组件实例仍存在 (供其他平台 share 同一份代码),
|
||||
但模板不会渲染任何内容, 鸿蒙用户看不到自建弹窗。
|
||||
-->
|
||||
<!-- #endif-->
|
||||
<!-- #ifndef MP-HARMONY -->
|
||||
<view>
|
||||
<view class="privacy-mask" v-if="showPop">
|
||||
<view class="privacy-wrap">
|
||||
@@ -15,7 +6,7 @@
|
||||
<view class="privacy-desc">
|
||||
{{ i18n.descPrefix }}
|
||||
<text class="privacy-link" @tap="openPrivacyContract">{{ i18n.privacyContractName }}</text>
|
||||
<!-- #ifdef QUICKAPP-WEBVIEW || H5 -->
|
||||
<!-- #ifdef QUICKAPP-WEBVIEW || H5 || MP-HARMONY -->
|
||||
<text class="privacy-link" @tap="openPrivacyService">{{ i18n.privacyServiceName }}</text>
|
||||
<!-- #endif -->
|
||||
{{ i18n.descSuffix }}
|
||||
@@ -26,15 +17,13 @@
|
||||
<button id="agree-btn" class="privacy-button-btn bg-agree" open-type="agreePrivacyAuthorization"
|
||||
@agreeprivacyauthorization="handleAgree">{{ i18n.agreeBtn }}</button>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 -->
|
||||
<button id="agree-btn" class="privacy-button-btn bg-agree" @tap="handleAgree">{{ i18n.agreeBtn
|
||||
}}</button>
|
||||
<!-- #ifdef H5 || MP-HARMONY -->
|
||||
<button id="agree-btn" class="privacy-button-btn bg-agree" @tap="handleAgree">{{ i18n.agreeBtn }}</button>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -59,12 +48,6 @@ export default {
|
||||
// 平台提供的隐私协议名称(由 wx.getPrivacySetting 等异步设置)
|
||||
// 弹窗前才参与 i18n.privacyContractName 的填充
|
||||
platformContractName: '',
|
||||
// 平台协议名称就绪 Promise:
|
||||
// created() 中发起异步查询, success/fail 时 resolve
|
||||
// popUp() 中 await 此 Promise 后再调 prepareI18n(), 保证 $lang() 与
|
||||
// platformContractName 都已经准备好, 不会出现"空串被永久缓存"的问题
|
||||
// (与 i18n-lazy mixin 的"boot 完成才能 prepareI18n"是同源契约)
|
||||
platformContractNameReady: null,
|
||||
// 弹窗文案: 模板只读 i18n.xxx
|
||||
// 初值为空串, 弹窗前在 popUp() 中**同步**填充
|
||||
// 此时 App.vue 已 await boot() + await PrivacyManager.show() 完成,
|
||||
@@ -87,22 +70,10 @@ export default {
|
||||
// 所以 listener 可在 created 注册, 但 callback 仍要 nextTick 渲染
|
||||
created() {
|
||||
try {
|
||||
// 创建平台协议名称就绪 Promise
|
||||
// 后续 wx/has/uni.* 三端的异步 API 都会挂到这个 promise 上 resolve,
|
||||
// popUp() 中 await 后再 prepareI18n(), 保证 platformContractName 已就绪
|
||||
// (与 i18n-lazy mixin 的"boot 完成才能 prepareI18n"契约同源)
|
||||
this.platformContractNameReady = new Promise((resolve) => {
|
||||
this._resolvePlatformContractName = resolve
|
||||
})
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
wx.getPrivacySetting({
|
||||
success: (res) => {
|
||||
this.platformContractName = res.privacyContractName || ''
|
||||
this._resolvePlatformContractName && this._resolvePlatformContractName()
|
||||
},
|
||||
fail: () => {
|
||||
this._resolvePlatformContractName && this._resolvePlatformContractName()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
@@ -114,28 +85,13 @@ export default {
|
||||
if (res.privacyContractName) {
|
||||
this.platformContractName = res.privacyContractName
|
||||
}
|
||||
this._resolvePlatformContractName && this._resolvePlatformContractName()
|
||||
},
|
||||
fail: () => {
|
||||
this._resolvePlatformContractName && this._resolvePlatformContractName()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// 无 API 时立即 resolve (避免 await 永久阻塞)
|
||||
this._resolvePlatformContractName && this._resolvePlatformContractName()
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙元服务: 已接入平台隐私托管, 隐私协议名称由鸿蒙系统管理
|
||||
// 应用层不再调 uni.getPrivacySetting (避免和系统托管弹窗冲突)
|
||||
// 立即 resolve, 让 popUp 的 await 不永久阻塞
|
||||
this._resolvePlatformContractName && this._resolvePlatformContractName()
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
// H5 无平台隐私 API, 立即 resolve
|
||||
this._resolvePlatformContractName && this._resolvePlatformContractName()
|
||||
// 鸿蒙元服务: 隐私授权由系统管理, 无需应用层查询 privacyContractName
|
||||
// #endif
|
||||
|
||||
// 提前注册 listener (晚于 created 早于 mounted, 减少事件丢失窗口)
|
||||
@@ -223,14 +179,14 @@ export default {
|
||||
* 统一弹窗 + 关闭其他页面的弹窗
|
||||
* 通过 uni.$emit('privacy:forceClose') 通知其他实例关闭
|
||||
*/
|
||||
async showPrivacyPopup() {
|
||||
showPrivacyPopup() {
|
||||
// 先通知其他实例关闭
|
||||
try { uni.$emit('privacy:forceClose'); } catch (e) { }
|
||||
try { uni.$emit('privacy:forceClose'); } catch (e) {}
|
||||
// 直接弹窗: boot 阶段已经保证 bundle 完整
|
||||
// 完整性校验在 lang.js _bootImpl() 中完成(检查 defaultAppNameMp 等关键 key)
|
||||
// 不完整时 force 重试, 仍不完整则报错不静默回退
|
||||
// 所以这里无需再检测/自愈, 信任 boot 已就绪
|
||||
await this.popUp()
|
||||
this.popUp()
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -309,8 +265,7 @@ export default {
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙元服务: 已接入平台隐私托管, 系统级弹窗由鸿蒙自动弹出
|
||||
// 不再监听 uni.onPrivacyAuthorizationChange (避免和系统托管冲突)
|
||||
// 鸿蒙元服务: 隐私授权由系统自动管理, 无需应用层监听
|
||||
// #endif
|
||||
},
|
||||
|
||||
@@ -329,22 +284,22 @@ export default {
|
||||
*/
|
||||
closeOtherPagePopUp(closePopUp) {
|
||||
// 通过全局事件通知所有页面关闭弹窗
|
||||
try { uni.$emit('privacy:closeOthers', this._uid); } catch (e) { }
|
||||
try { uni.$emit('privacy:closeOthers', this._uid); } catch (e) {}
|
||||
},
|
||||
|
||||
openPrivacyContract() {
|
||||
// #ifdef MP-WEIXIN
|
||||
wx.openPrivacyContract({
|
||||
success(res) { },
|
||||
fail(err) { }
|
||||
success(res) {},
|
||||
fail(err) {}
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifdef QUICKAPP-WEBVIEW
|
||||
if (typeof has != 'undefined' && has?.openPrivacySetting) {
|
||||
has.openPrivacySetting({
|
||||
success: (res) => { },
|
||||
fail: (err) => { }
|
||||
success: (res) => {},
|
||||
fail: (err) => {}
|
||||
});
|
||||
} else {
|
||||
this.$util.redirectTo(privacyContractPage);
|
||||
@@ -352,8 +307,8 @@ export default {
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙元服务: 走平台隐私托管, 协议查看由鸿蒙系统接管
|
||||
// (隐私弹窗本身不再在鸿蒙上弹出, 这里也基本不会被触发)
|
||||
// 鸿蒙元服务: 使用应用内协议页面
|
||||
this.$util.redirectTo(privacyContractPage);
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
@@ -365,8 +320,8 @@ export default {
|
||||
// #ifdef QUICKAPP-WEBVIEW
|
||||
if (typeof has != 'undefined' && has?.openPrivacySetting) {
|
||||
has.openPrivacySetting({
|
||||
success: (res) => { },
|
||||
fail: (err) => { }
|
||||
success: (res) => {},
|
||||
fail: (err) => {}
|
||||
});
|
||||
} else {
|
||||
this.$util.redirectTo(privacyServicePage);
|
||||
@@ -374,7 +329,7 @@ export default {
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-HARMONY
|
||||
// 鸿蒙元服务: 走平台隐私托管, 协议查看由鸿蒙系统接管
|
||||
this.$util.redirectTo(privacyServicePage);
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
@@ -389,7 +344,7 @@ export default {
|
||||
this.privacyResolves.clear()
|
||||
|
||||
// 先通知其他实例关闭, 再关闭自己, 避免叠层
|
||||
try { uni.$emit('privacy:forceClose'); } catch (e) { }
|
||||
try { uni.$emit('privacy:forceClose'); } catch (e) {}
|
||||
this.disPopUp()
|
||||
|
||||
// 修复: 拒绝时不写 privacyAgreed, 允许下次再弹
|
||||
@@ -414,7 +369,7 @@ export default {
|
||||
this.privacyResolves.clear()
|
||||
|
||||
// 先通知其他实例关闭, 再关闭自己
|
||||
try { uni.$emit('privacy:forceClose'); } catch (e) { }
|
||||
try { uni.$emit('privacy:forceClose'); } catch (e) {}
|
||||
this.disPopUp()
|
||||
|
||||
// 由 PrivacyManager 管理 storage
|
||||
@@ -435,9 +390,9 @@ export default {
|
||||
*/
|
||||
i18nPrepareFn() {
|
||||
let appName, privacyContractName, privacyServiceName;
|
||||
|
||||
|
||||
// 关键: appName 必须先解析为字符串, 才能作为 descPrefix/descSuffix 的参数
|
||||
// #ifdef H5
|
||||
// #ifdef H5 || MP-HARMONY
|
||||
appName = this.$lang('components.privacyPopup.defaultAppNameH5');
|
||||
privacyContractName = this.platformContractName || this.$lang('components.privacyPopup.defaultContractNameH5');
|
||||
privacyServiceName = this.$lang('components.privacyPopup.defaultServiceNameH5');
|
||||
@@ -461,33 +416,20 @@ export default {
|
||||
return result;
|
||||
},
|
||||
|
||||
async popUp() {
|
||||
popUp() {
|
||||
// 弹窗前**一次性**计算所有 i18n 文案
|
||||
// 必须先等两类数据准备好:
|
||||
// 1) Lang.boot() 完成 — 由 i18n-lazy mixin 隐含保证
|
||||
// (调用方 App.vue 在 await PrivacyManager.show() 后才触发弹窗,
|
||||
// PrivacyManager.show() await 了 boot 守卫)
|
||||
// 2) platformContractName 就绪 — 微信/快应用/鸿蒙三端的 API 都是异步的,
|
||||
// success 回调时机不确定, 直接 prepareI18n() 会读到空字符串, 导致
|
||||
// "空串被永久缓存" (与 i18n-lazy mixin 警告的问题同源)
|
||||
// 因此这里 await platformContractNameReady, 然后再 prepareI18n。
|
||||
if (this.platformContractNameReady) {
|
||||
try {
|
||||
await this.platformContractNameReady
|
||||
} catch (e) {
|
||||
// 防御性: 即便 ready 出错也继续 (走 fallback 默认文案)
|
||||
}
|
||||
}
|
||||
// boot 阶段已用 isBundleComplete() 保证 bundle 完整
|
||||
// $lang() 返回真实翻译(效果与 H5 一致)
|
||||
this.prepareI18n();
|
||||
this.showPop = true;
|
||||
// 通知 PrivacyManager 弹窗实际显示状态 (用于 rules-updated 双阶段重评估)
|
||||
try { uni.$emit && uni.$emit('privacy:popup-state', { showing: true }) } catch (e) { }
|
||||
try { uni.$emit && uni.$emit('privacy:popup-state', { showing: true }) } catch (e) {}
|
||||
},
|
||||
|
||||
disPopUp() {
|
||||
this.showPop = false;
|
||||
// 通知 PrivacyManager 弹窗实际显示状态 (用于 rules-updated 双阶段重评估)
|
||||
try { uni.$emit && uni.$emit('privacy:popup-state', { showing: false }) } catch (e) { }
|
||||
try { uni.$emit && uni.$emit('privacy:popup-state', { showing: false }) } catch (e) {}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
@@ -496,11 +438,6 @@ export default {
|
||||
uni.$off('privacy:show', this.handlePrivacyShow)
|
||||
uni.$off('privacy:forceClose', this.handleForceClose)
|
||||
uni.$off('privacy:re-evaluate', this.handleReEvaluate)
|
||||
// 销毁时强制 resolve, 避免 popUp() 中的 await 永久挂起 (例如用户提前离开页面)
|
||||
if (typeof this._resolvePlatformContractName === 'function') {
|
||||
this._resolvePlatformContractName()
|
||||
this._resolvePlatformContractName = null
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -554,7 +491,7 @@ export default {
|
||||
.privacy-button-btn {
|
||||
color: #FFF;
|
||||
font-size: 30rpx;
|
||||
/* #ifdef QUICKAPP-WEBVIEW || H5 */
|
||||
/* #ifdef QUICKAPP-WEBVIEW || H5 || MP-HARMONY */
|
||||
font-size: 28rpx;
|
||||
/* #endif */
|
||||
font-weight: 500;
|
||||
|
||||
5176
lang-bundle.json
5176
lang-bundle.json
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,10 @@ export const lang = {
|
||||
|
||||
// 底部选项卡
|
||||
tabBar: {
|
||||
home: 'Home',
|
||||
category: 'Category',
|
||||
cart: 'Cart',
|
||||
member: 'Me',
|
||||
home: 'index',
|
||||
category: 'category',
|
||||
cart: 'cart',
|
||||
member: 'member',
|
||||
},
|
||||
|
||||
//公共
|
||||
@@ -54,15 +54,11 @@ export const lang = {
|
||||
serviceUnavailable: 'Navigation service temporarily unavailable',
|
||||
},
|
||||
|
||||
// diy custom page (diy-specific copy, generic HTTP errors reuse http.*)
|
||||
// diy custom page
|
||||
diy: {
|
||||
networkErrorTitle: 'Notice',
|
||||
networkError: 'Network error, please try again later',
|
||||
retry: 'Retry',
|
||||
noDiyData: 'Custom page data not configured',
|
||||
locationNotAvailable: 'Location not available',
|
||||
// detailed data-load failure template (diy-specific, {reason} is filled by generic http.*)
|
||||
loadFailTpl: 'Failed to load page data: {reason}, please try again later',
|
||||
},
|
||||
|
||||
//customer service
|
||||
@@ -112,7 +108,6 @@ export const lang = {
|
||||
notSupported: 'Current environment does not support WeChat Channel',
|
||||
missingFields: 'Missing required field: {field}',
|
||||
playFailed: 'WeChat video playback failed!',
|
||||
wechatMpOnly: 'WeChat Channel only plays in WeChat Mini Program',
|
||||
},
|
||||
|
||||
// uniapp utility functions
|
||||
@@ -139,18 +134,12 @@ export const lang = {
|
||||
requestFailed: '{url} request failed. {err}',
|
||||
// network timeout
|
||||
timeout: 'Network request timeout, please try again later',
|
||||
// network connection failed / generic network error (unified copy, reused by diy and other generic scenarios)
|
||||
networkError: 'Network error, please try again later',
|
||||
// network connection failed
|
||||
networkError: 'Network connection failed, please check your network',
|
||||
// server error
|
||||
serverError: 'Server error, please try again later',
|
||||
// API not found (HTTP 404)
|
||||
notFound: 'API not found',
|
||||
// no access permission (HTTP 401 / 403)
|
||||
auth: 'No access permission',
|
||||
serverError: 'Service temporarily unavailable, please try again later',
|
||||
// request aborted
|
||||
aborted: 'Request cancelled',
|
||||
// unknown error (errorType fallback, e.g. non-5xx/404/401/403 HTTP status)
|
||||
unknown: 'Request failed, please try again later',
|
||||
// unknown error
|
||||
unknownError: 'Request failed, please try again later',
|
||||
},
|
||||
@@ -258,14 +247,7 @@ export const lang = {
|
||||
addToCartFailMin: 'Minimum purchase quantity not met',
|
||||
addToCartFailStock: 'Insufficient Stock',
|
||||
|
||||
// generic submit/version/loading text (key lives in root because common.js's lang object IS the common namespace)
|
||||
submitSuccess: 'Submit Success',
|
||||
submitFail: 'Submit Failed',
|
||||
versionInfo: 'Version Info',
|
||||
commitId: 'Commit ID',
|
||||
buildTime: 'Build Time',
|
||||
loading: 'Loading...',
|
||||
|
||||
// 咨询
|
||||
consult: 'Consult Now',
|
||||
consultSuccess: 'Consultation Successful',
|
||||
consultFail: 'Consultation Failed',
|
||||
|
||||
@@ -115,7 +115,6 @@ export const lang = {
|
||||
|
||||
// Product Message
|
||||
productMessage: 'Product Message',
|
||||
formEmptyTip: 'No message fields to fill',
|
||||
|
||||
// AI Customer Service
|
||||
aiCustomerService: 'AI Assistant',
|
||||
|
||||
@@ -3,6 +3,5 @@ export const lang = {
|
||||
title: 'Payment Method',
|
||||
paymentAmount: 'Payment Amount',
|
||||
confirmPayment: 'Confirm Payment',
|
||||
seeOrder: 'View Order',
|
||||
payInfoNotFound: 'Payment info not found!'
|
||||
seeOrder: 'View Order'
|
||||
}
|
||||
@@ -61,15 +61,11 @@ export const lang = {
|
||||
weChatWorkKefu: '企业微信客服',
|
||||
},
|
||||
|
||||
// diy 自定义页面(diy 专属文案,通用网络/接口错误复用 http.*)
|
||||
// diy 自定义页面
|
||||
diy: {
|
||||
networkErrorTitle: '提示',
|
||||
networkError: '网络异常,请稍后重试',
|
||||
retry: '重试',
|
||||
noDiyData: '未配置自定义页面数据',
|
||||
locationNotAvailable: '未获取到定位',
|
||||
// 数据加载失败详细文案(diy 专属模板,内部 {reason} 由通用 http.* 填充)
|
||||
loadFailTpl: '页面数据加载失败:{reason},请稍后重试',
|
||||
},
|
||||
|
||||
// 客服服务
|
||||
@@ -112,7 +108,6 @@ export const lang = {
|
||||
notSupported: '当前环境不支持微信视频号',
|
||||
missingFields: '缺少必要字段: {field}',
|
||||
playFailed: '微信视频播放失败!',
|
||||
wechatMpOnly: '视频号仅支持在微信小程序环境中播放',
|
||||
},
|
||||
|
||||
// uniapp 工具函数
|
||||
@@ -139,18 +134,12 @@ export const lang = {
|
||||
requestFailed: '{url}请求失败. {err}',
|
||||
// 网络超时
|
||||
timeout: '网络请求超时,请稍后重试',
|
||||
// 网络连接失败 / 通用网络异常(统一文案,供 diy 等通用场景复用)
|
||||
networkError: '网络异常,请稍后重试',
|
||||
// 网络连接失败
|
||||
networkError: '网络连接失败,请检查网络设置',
|
||||
// 服务器错误
|
||||
serverError: '服务器异常,请稍后重试',
|
||||
// 接口不存在(HTTP 404)
|
||||
notFound: '接口不存在',
|
||||
// 无访问权限(HTTP 401 / 403)
|
||||
auth: '无访问权限',
|
||||
serverError: '服务暂时不可用,请稍后重试',
|
||||
// 请求被中断
|
||||
aborted: '请求已取消',
|
||||
// 未知错误(errorType 分类兜底,如非 5xx/404/401/403 的 HTTP 状态码)
|
||||
unknown: '请求失败,请稍后重试',
|
||||
// 未知错误
|
||||
unknownError: '请求失败,请稍后重试',
|
||||
},
|
||||
@@ -256,14 +245,6 @@ export const lang = {
|
||||
addToCartFailMin: '已达到最小购买数量',
|
||||
addToCartFailStock: '库存不足',
|
||||
|
||||
// 通用提交/版本/loading 文案(原本该走 common.* 命名空间,因 common.js 整体即 common namespace,放在 root)
|
||||
submitSuccess: '提交成功',
|
||||
submitFail: '提交失败',
|
||||
versionInfo: '版本信息',
|
||||
commitId: '提交 ID',
|
||||
buildTime: '构建时间',
|
||||
loading: '加载中...',
|
||||
|
||||
consult: '立即咨询',
|
||||
consultSuccess: '咨询成功',
|
||||
consultFail: '咨询失败',
|
||||
|
||||
@@ -108,7 +108,6 @@ export const lang = {
|
||||
|
||||
// 商品留言
|
||||
productMessage: '商品留言',
|
||||
formEmptyTip: '暂无需填写的留言字段',
|
||||
|
||||
// 智能客服
|
||||
aiCustomerService: '智能客服',
|
||||
|
||||
@@ -3,6 +3,5 @@ export const lang = {
|
||||
title: '支付方式',
|
||||
paymentAmount: '支付金额',
|
||||
confirmPayment: '确认支付',
|
||||
seeOrder: '查看订单',
|
||||
payInfoNotFound: '未获取到支付信息!'
|
||||
seeOrder: '查看订单'
|
||||
}
|
||||
|
||||
8
main.js
8
main.js
@@ -80,18 +80,10 @@ Vue.mixin(MpShareDefault);
|
||||
// lang.js 在翻译覆写更新时 commit('setComponentRefresh'),
|
||||
// golbalConfig.js 已把 componentRefresh 注册为全局 computed,
|
||||
// 在此 watch 它并 $forceUpdate,触发所有页面内 {{$lang('xxx')}} 重新求值
|
||||
// 同时调用 lang.refresh() 让当前页面的 navigationBarTitleText 跟随语种变化
|
||||
// (pages.json 里的硬编码中文标题需要被 $lang('title') 覆盖)
|
||||
Vue.mixin({
|
||||
watch: {
|
||||
componentRefresh() {
|
||||
this.$forceUpdate();
|
||||
try {
|
||||
if (this.$langConfig && typeof this.$langConfig.refresh === 'function') {
|
||||
// emit=false: lang.js 已 emit 过,这里避免重复 store.commit
|
||||
this.$langConfig.refresh(false);
|
||||
}
|
||||
} catch (e) { /* 容错:refresh 失败不影响主流程 */ }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -136,23 +136,7 @@
|
||||
"sassImplementationName" : "dart-sass",
|
||||
"mp-harmony" : {
|
||||
"distribute" : {
|
||||
"bundleName" : "com.atomicservice.6917607804641099460"
|
||||
},
|
||||
// 隐私托管: 元服务必须使用 AGC 后台的标准化隐私声明托管服务,
|
||||
// 弹窗由鸿蒙系统自动弹出, 应用层不能自建.
|
||||
// 下方的 privacy 块仅作为应用内协议页兜底 (例如用户在系统设置里手动查看协议),
|
||||
// 不参与首次启动弹窗逻辑.
|
||||
"privacy" : {
|
||||
"privacyContractUrl" : "YOUR_DOMAIN/api/agreementview/privacy?uniacid=YOUR_UNIACID",
|
||||
"serviceContractUrl" : "YOUR_DOMAIN/api/agreementview/service?uniacid=YOUR_UNIACID",
|
||||
"privacyContractName" : {
|
||||
"zh" : "隐私政策",
|
||||
"en" : "Privacy Policy"
|
||||
},
|
||||
"serviceContractName" : {
|
||||
"zh" : "用户服务协议",
|
||||
"en" : "User Service Agreement"
|
||||
}
|
||||
"bundleName" : "com.atomicservice.6917577984608507983"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
164
package.json
164
package.json
@@ -1,92 +1,72 @@
|
||||
{
|
||||
"scripts": {
|
||||
"mp-weixin": "node scripts/mp-weixin.patch.js",
|
||||
"i18n:check": "node scripts/i18n-check.js",
|
||||
"test": "node scripts/test-gate.js",
|
||||
"test:util": "node --test tests/util.test.js",
|
||||
"prebuild": "npm run test",
|
||||
"prestart": "npm run test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dart-sass": "^1.25.0",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"zion-uniapp-mp-load-package": "^1.0.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"jweixin-module": "^1.6.0"
|
||||
},
|
||||
"uni-app": {
|
||||
"scripts": {
|
||||
"weixin": {
|
||||
"title": "微信小程序自定义发行",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "mp-weixin"
|
||||
},
|
||||
"define": {
|
||||
"MP_WEIXIN": true,
|
||||
"PRODUCTION": true
|
||||
}
|
||||
},
|
||||
"h5-xcx20.5g-quickapp.com": {
|
||||
"title": "H5-xcx20.5g-quickapp.com",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "h5",
|
||||
"UNI_APP_DOMAIN": "https://xcx20.5g-quickapp.com/"
|
||||
},
|
||||
"define": {
|
||||
"PRODUCTION": true
|
||||
}
|
||||
},
|
||||
"h5-xcx.aigc-quickapp.com": {
|
||||
"title": "H5-xcx.aigc-quickapp.com",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "h5",
|
||||
"UNI_APP_DOMAIN": "https://xcx.aigc-quickapp.com/"
|
||||
},
|
||||
"define": {
|
||||
"PRODUCTION": true
|
||||
}
|
||||
},
|
||||
"h5-5g.aigc-quickapp.com": {
|
||||
"title": "H5-5g.aigc-quickapp.com",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "h5",
|
||||
"UNI_APP_DOMAIN": "https://5g.aigc-quickapp.com/"
|
||||
},
|
||||
"define": {
|
||||
"PRODUCTION": true
|
||||
}
|
||||
},
|
||||
"h5-test.aigc-quickapp.com": {
|
||||
"title": "H5-test.aigc-quickapp.com",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "h5",
|
||||
"UNI_APP_DOMAIN": "https://test.aigc-quickapp.com/"
|
||||
},
|
||||
"define": {
|
||||
"PRODUCTION": true
|
||||
}
|
||||
},
|
||||
"http://localhost:8050/": {
|
||||
"title": "H5-localhost:8050",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "h5",
|
||||
"UNI_APP_DOMAIN": "http://localhost:8050/"
|
||||
},
|
||||
"define": {
|
||||
"PRODUCTION": true
|
||||
}
|
||||
},
|
||||
"http://192.168.8.118:8050/": {
|
||||
"title": "H5-192.168.8.118:8050",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "h5",
|
||||
"UNI_APP_DOMAIN": "http://192.168.8.118:8050/"
|
||||
},
|
||||
"define": {
|
||||
"PRODUCTION": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"scripts": {
|
||||
"mp-weixin": "node scripts/mp-weixin.patch.js",
|
||||
"i18n:check": "node scripts/i18n-check.js",
|
||||
"test": "node scripts/test-gate.js",
|
||||
"test:util": "node --test tests/util.test.js",
|
||||
"prebuild": "npm run test",
|
||||
"prestart": "npm run test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dart-sass": "^1.25.0",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"zion-uniapp-mp-load-package": "^1.0.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"jweixin-module": "^1.6.0"
|
||||
},
|
||||
"uni-app": {
|
||||
"scripts": {
|
||||
"weixin": {
|
||||
"title": "微信小程序自定义发行",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "mp-weixin"
|
||||
},
|
||||
"define": {
|
||||
"MP_WEIXIN": true,
|
||||
"PRODUCTION": true
|
||||
}
|
||||
},
|
||||
"h5-xcx20.5g-quickapp.com": {
|
||||
"title": "H5-xcx20.5g-quickapp.com",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "h5",
|
||||
"UNI_APP_DOMAIN": "https://xcx20.5g-quickapp.com/"
|
||||
},
|
||||
"define": {
|
||||
"PRODUCTION": true
|
||||
}
|
||||
},
|
||||
"h5-xcx.aigc-quickapp.com": {
|
||||
"title": "H5-xcx.aigc-quickapp.com",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "h5",
|
||||
"UNI_APP_DOMAIN": "https://xcx.aigc-quickapp.com/"
|
||||
},
|
||||
"define": {
|
||||
"PRODUCTION": true
|
||||
}
|
||||
},
|
||||
"h5-5g.aigc-quickapp.com": {
|
||||
"title": "H5-5g.aigc-quickapp.com",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "h5",
|
||||
"UNI_APP_DOMAIN": "https://5g.aigc-quickapp.com/"
|
||||
},
|
||||
"define": {
|
||||
"PRODUCTION": true
|
||||
}
|
||||
},
|
||||
"h5-test.aigc-quickapp.com": {
|
||||
"title": "H5-test.aigc-quickapp.com",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "h5",
|
||||
"UNI_APP_DOMAIN": "https://test.aigc-quickapp.com/"
|
||||
},
|
||||
"define": {
|
||||
"PRODUCTION": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
421
pages.json
421
pages.json
File diff suppressed because it is too large
Load Diff
@@ -2,15 +2,6 @@
|
||||
<view :style="themeColor">
|
||||
<view :style="{ backgroundColor: bgColor, minHeight: openBottomNav ? 'calc(100vh - 55px)' : '' }"
|
||||
class="page-img">
|
||||
<!-- diy 数据网络加载失败提示(页面内横幅,各端可靠,悬浮在最上层) -->
|
||||
<view v-if="diyLoadError" class="diy-load-error">
|
||||
<view class="diy-load-error__body">
|
||||
<text class="diy-load-error__msg">{{ diyLoadErrorMsg || '网络异常,请稍后重试' }}</text>
|
||||
<text v-if="diyLoadErrorUrl" class="diy-load-error__url" selectable user-select>{{ diyLoadErrorUrl }}</text>
|
||||
</view>
|
||||
<view class="diy-load-error__btn" @tap="retryDiyInfo">{{ $lang('diy.retry') || '重试' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="site-info-box"
|
||||
v-if="$util.isWeiXin() && followOfficialAccount && followOfficialAccount.isShow && wechatQrcode">
|
||||
<view class="site-info">
|
||||
@@ -188,52 +179,6 @@ export default {
|
||||
z-index: 122;
|
||||
}
|
||||
|
||||
.diy-load-error {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 28rpx;
|
||||
padding-top: calc(24rpx + var(--status-bar-height, 0px));
|
||||
background-color: #fff5f5;
|
||||
border-bottom: 1rpx solid #ffccc7;
|
||||
}
|
||||
|
||||
.diy-load-error__body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.diy-load-error__msg {
|
||||
font-size: 26rpx;
|
||||
color: #cf1322;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.diy-load-error__url {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.diy-load-error__btn {
|
||||
margin-left: 20rpx;
|
||||
padding: 10rpx 28rpx;
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
background-color: #cf1322;
|
||||
border-radius: 32rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.small-bot swiper {
|
||||
width: 560rpx;
|
||||
height: 784rpx;
|
||||
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
onShow() {
|
||||
// 获取关注公众号二维码
|
||||
this.getFollowQrcode();
|
||||
|
||||
|
||||
// 重新获取店铺信息
|
||||
this.shopInfo = uni.getStorageSync('shopInfo');
|
||||
},
|
||||
@@ -160,10 +160,10 @@ export default {
|
||||
} catch (e) {}
|
||||
if (v && v.commitId) {
|
||||
uni.showModal({
|
||||
title: this.$lang('versionInfo'),
|
||||
content: `${this.$lang('commitId')}: ${v.commitId}\n${this.$lang('buildTime')}: ${v.buildTime}\n${Config.baseUrl}`,
|
||||
showCancel: false
|
||||
});
|
||||
title: '版本信息',
|
||||
content: `提交 ID: ${v.commitId}\n构建时间: ${v.buildTime}\n${Config.baseUrl}`,
|
||||
showCancel: false
|
||||
});
|
||||
} else {
|
||||
console.error('未获取到构建版本信息');
|
||||
}
|
||||
|
||||
@@ -1,31 +1,7 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 骨架屏:商品详情加载时按结构分段占位(banner / 价格 / 标题 / 规格 / 商家 / 详情 / 推荐)
|
||||
关闭条件: loading=false 且已拿到真实商品数据(goods_id>0); 任一不满足都保留骨架屏 -->
|
||||
<view v-if="loading || !hasRealData" class="goods-detail-skeleton">
|
||||
<x-skeleton data-component-name="goods-detail-view-banner" :loading="true" :configs="skeletonBanner"></x-skeleton>
|
||||
<x-skeleton data-component-name="goods-detail-view-price" :loading="true" :configs="skeletonPrice"></x-skeleton>
|
||||
<x-skeleton data-component-name="goods-detail-view-title" :loading="true" :configs="skeletonTitle"></x-skeleton>
|
||||
<x-skeleton data-component-name="goods-detail-view-sku" :loading="true" :configs="skeletonSku"></x-skeleton>
|
||||
<x-skeleton data-component-name="goods-detail-view-merch" :loading="true" :configs="skeletonMerch"></x-skeleton>
|
||||
<x-skeleton data-component-name="goods-detail-view-detail" :loading="true" :configs="skeletonDetail"></x-skeleton>
|
||||
<x-skeleton data-component-name="goods-detail-view-recommend" :loading="true" :configs="skeletonRecommend"></x-skeleton>
|
||||
<!-- 底部 action 骨架占位:与 ns-goods-action 同样 fixed bottom, 避免数据未到时底部跳变 -->
|
||||
<view class="goods-detail-skeleton-action">
|
||||
<view class="skeleton-action-icons">
|
||||
<view class="skeleton-action-icon" v-for="i in 2" :key="'sk-act-icon-' + i"></view>
|
||||
</view>
|
||||
<view class="skeleton-action-btn"></view>
|
||||
<view class="skeleton-action-btn primary"></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 修复鸿蒙元服务: 移除无效的 scroll-y="true" 属性 (scroll-y 是 <scroll-view> 的指令属性)
|
||||
鸿蒙 JSVM 早期版本遇到无效 attribute 会让 view 子树静默不渲染, 导致整个详情页空白 -->
|
||||
<view v-else class="goods-detail-wrap">
|
||||
<!-- 操作区域 slot: 与 .goods-detail 同一 v-else 分支, 避免骨架屏状态下被父组件渲染真实按钮(基于默认 goods_state=0 显示"该商品已下架") -->
|
||||
<slot name="action"></slot>
|
||||
<view class="goods-detail" :class="isIphoneX ? 'active' : ''">
|
||||
<view class="goods-container">
|
||||
<view scroll-y="true" class="goods-detail" :class="isIphoneX ? 'active' : ''">
|
||||
<view class="goods-container">
|
||||
<!-- AI客服悬浮按钮 -->
|
||||
<view class="ai-chat-float-btn" @tap.stop="$emit('aiChatClick')" v-if="showAIChatBtn">
|
||||
<image :src="aiAvatarUrl" mode="aspectFill" class="ai-avatar-icon" />
|
||||
@@ -59,8 +35,7 @@
|
||||
<!-- 商品视频 -->
|
||||
<view class="goods-video" :class="{ show: switchMedia == 'video' }">
|
||||
<video id="goodsVideo" :src="$util.img(goodsSkuDetail.video_url)"
|
||||
:poster="$util.img(goodsSkuDetail.sku_image, { size: 'big' })" objectFit="cover"
|
||||
playsinline webkit-playsinline x5-playsinline></video>
|
||||
:poster="$util.img(goodsSkuDetail.sku_image, { size: 'big' })" objectFit="cover"></video>
|
||||
</view>
|
||||
|
||||
<!-- 切换视频、图片 -->
|
||||
@@ -484,7 +459,7 @@
|
||||
<text>{{ $lang('goods.detail.detailView.generateSharePoster') }}</text>
|
||||
</button>
|
||||
</view>
|
||||
<!-- #ifdef H5 || MP-HARMONY -->
|
||||
<!-- #ifdef H5 -->
|
||||
<view class="share-box" @click="copyUrl">
|
||||
<button class="share-btn" :plain="true">
|
||||
<view class="iconfont icon-fuzhilianjie"></view>
|
||||
@@ -499,10 +474,11 @@
|
||||
</view>
|
||||
|
||||
<slot name="fixedbtn"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作区域 -->
|
||||
<slot name="action"></slot>
|
||||
<to-top v-if="showTop" @toTop="scrollToTopNative()"></to-top>
|
||||
<ns-login ref="login"></ns-login>
|
||||
</view>
|
||||
@@ -525,142 +501,11 @@ export default {
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
// 是否显示骨架屏(true:展示骨架;false:展示真实内容)
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
pengpaiFadeinOut
|
||||
},
|
||||
data() {
|
||||
// 商品详情骨架屏:模拟「轮播图 + 价格行 + 标题简介 + 规格行 + 商家 + 详情 + 推荐」
|
||||
return {
|
||||
// 1. 轮播图:方形 banner 占位
|
||||
skeletonBanner: {
|
||||
padding: '0',
|
||||
gridRows: 1,
|
||||
gridColumns: 1,
|
||||
gridRowsGap: '24rpx',
|
||||
headShow: true,
|
||||
headWidth: '100%',
|
||||
headHeight: '750rpx',
|
||||
headBorderRadius: '0',
|
||||
textShow: false
|
||||
},
|
||||
// 2. 价格行:一行价 + 一行原价
|
||||
skeletonPrice: {
|
||||
padding: '24rpx 24rpx 16rpx',
|
||||
gridRows: 2,
|
||||
gridColumns: 1,
|
||||
gridRowsGap: '16rpx',
|
||||
itemDirection: 'column',
|
||||
itemAlign: 'flex-start',
|
||||
headShow: false,
|
||||
textShow: true,
|
||||
textRows: 1,
|
||||
textRowsGap: '0',
|
||||
textWidth: ['40%', '30%'],
|
||||
textHeight: ['48rpx', '24rpx'],
|
||||
textBorderRadius: '6rpx'
|
||||
},
|
||||
// 3. 标题/简介:标题两行 + 简介 + 标签
|
||||
skeletonTitle: {
|
||||
padding: '8rpx 24rpx 24rpx',
|
||||
gridRows: 1,
|
||||
gridColumns: 1,
|
||||
gridRowsGap: '20rpx',
|
||||
itemDirection: 'column',
|
||||
itemAlign: 'flex-start',
|
||||
headShow: false,
|
||||
textShow: true,
|
||||
textRows: 4,
|
||||
textRowsGap: '16rpx',
|
||||
textWidth: ['90%', '60%', '100%', '40%'],
|
||||
textHeight: ['40rpx', '40rpx', '24rpx', '24rpx'],
|
||||
textBorderRadius: '6rpx'
|
||||
},
|
||||
// 4. 规格行:单行高度的小条
|
||||
skeletonSku: {
|
||||
padding: '20rpx 24rpx',
|
||||
gridRows: 1,
|
||||
gridColumns: 1,
|
||||
gridRowsGap: '0',
|
||||
itemDirection: 'row',
|
||||
itemAlign: 'center',
|
||||
headShow: true,
|
||||
headWidth: '120rpx',
|
||||
headHeight: '32rpx',
|
||||
headBorderRadius: '6rpx',
|
||||
textShow: true,
|
||||
textRows: 1,
|
||||
textRowsGap: '0',
|
||||
textWidth: '60%',
|
||||
textHeight: '28rpx',
|
||||
textBorderRadius: '6rpx',
|
||||
itemGap: '20rpx'
|
||||
},
|
||||
// 5. 商家:头像 + 名称
|
||||
skeletonMerch: {
|
||||
padding: '24rpx',
|
||||
gridRows: 1,
|
||||
gridColumns: 1,
|
||||
gridRowsGap: '0',
|
||||
itemDirection: 'row',
|
||||
itemAlign: 'center',
|
||||
headShow: true,
|
||||
headWidth: '80rpx',
|
||||
headHeight: '80rpx',
|
||||
headBorderRadius: '50%',
|
||||
textShow: true,
|
||||
textRows: 2,
|
||||
textRowsGap: '12rpx',
|
||||
textWidth: ['50%', '80%'],
|
||||
textHeight: ['28rpx', '20rpx'],
|
||||
textBorderRadius: '6rpx',
|
||||
itemGap: '24rpx'
|
||||
},
|
||||
// 6. 商品详情:多行段落
|
||||
skeletonDetail: {
|
||||
padding: '24rpx',
|
||||
gridRows: 1,
|
||||
gridColumns: 1,
|
||||
gridRowsGap: '0',
|
||||
itemDirection: 'column',
|
||||
itemAlign: 'flex-start',
|
||||
headShow: false,
|
||||
textShow: true,
|
||||
textRows: 6,
|
||||
textRowsGap: '16rpx',
|
||||
textWidth: ['100%', '100%', '90%', '100%', '80%', '60%'],
|
||||
textHeight: '24rpx',
|
||||
textBorderRadius: '6rpx'
|
||||
},
|
||||
// 7. 推荐:两列卡片缩略图
|
||||
skeletonRecommend: {
|
||||
padding: '24rpx',
|
||||
gridRows: 1,
|
||||
gridColumns: 2,
|
||||
gridRowsGap: '24rpx',
|
||||
gridColumnsGap: '24rpx',
|
||||
itemDirection: 'column',
|
||||
itemAlign: 'flex-start',
|
||||
headShow: true,
|
||||
headWidth: '100%',
|
||||
headHeight: '320rpx',
|
||||
headBorderRadius: '12rpx',
|
||||
textShow: true,
|
||||
textRows: 2,
|
||||
textRowsGap: '12rpx',
|
||||
textWidth: ['100%', '70%'],
|
||||
textHeight: ['24rpx', '24rpx'],
|
||||
textBorderRadius: '6rpx',
|
||||
itemGap: '12rpx'
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
aiAvatarUrl() {
|
||||
return this.$util.img(IMAGE_CONFIG.aiAvatar);
|
||||
@@ -679,11 +524,6 @@ export default {
|
||||
hasDetailContent() {
|
||||
const hasPdf = this.goodsSkuDetail && this.goodsSkuDetail.pdf_url;
|
||||
return hasPdf || this.hasHtmlTab;
|
||||
},
|
||||
// 是否有真实商品数据: goods_id > 0 才算拿到商品详情
|
||||
// 用于「未拿到任何数据时不显示真实内容」, 避免加载失败/异常时切到空详情页
|
||||
hasRealData() {
|
||||
return this.goodsSkuDetail && Number(this.goodsSkuDetail.goods_id) > 0;
|
||||
}
|
||||
},
|
||||
mixins: [scroll, detail]
|
||||
@@ -692,79 +532,6 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/common/css/goods_detail.scss';
|
||||
|
||||
/* 骨架屏外层:与商品详情整体上对齐,左右内边距由各 skeleton 自己控制 */
|
||||
.goods-detail-skeleton {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 底部 action 骨架占位:与 ns-goods-action 同样 fixed bottom, 高度接近真实按钮 */
|
||||
.goods-detail-skeleton-action {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
z-index: 9;
|
||||
padding: 0 16rpx;
|
||||
box-sizing: border-box;
|
||||
border-top: 1rpx solid #f2f2f2;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.goods-detail-skeleton-action .skeleton-action-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.goods-detail-skeleton-action .skeleton-action-icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #EAEDF5;
|
||||
background: linear-gradient(90deg, #EAEDF5 25%, #F9FAFF 37%, #EAEDF5 50%);
|
||||
background-size: 400% 100%;
|
||||
animation: skActAnim 1.8s ease infinite;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.goods-detail-skeleton-action .skeleton-action-icon:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.goods-detail-skeleton-action .skeleton-action-btn {
|
||||
flex: 1;
|
||||
height: 70rpx;
|
||||
border-radius: 10rpx;
|
||||
margin-right: 16rpx;
|
||||
background-color: #EAEDF5;
|
||||
background: linear-gradient(90deg, #EAEDF5 25%, #F9FAFF 37%, #EAEDF5 50%);
|
||||
background-size: 400% 100%;
|
||||
animation: skActAnim 1.8s ease infinite;
|
||||
}
|
||||
|
||||
.goods-detail-skeleton-action .skeleton-action-btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.goods-detail-skeleton-action .skeleton-action-btn.primary {
|
||||
background: linear-gradient(90deg, #d8d8d8 25%, #ececec 37%, #d8d8d8 50%);
|
||||
background-size: 400% 100%;
|
||||
}
|
||||
|
||||
@keyframes skActAnim {
|
||||
0% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.ai-chat-float-btn {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<view :style="themeColor">
|
||||
<!-- 使用 item.id / componentName 稳定 key 避免 v-for 在 diyData 整体重赋值时把组件整个销毁重建, 引发频闪 -->
|
||||
<view v-if="diyData && diyData.value && diyData.value.length">
|
||||
<view v-show="diyData && diyData.value && diyData.value.length">
|
||||
<block v-for="(item, index) in diyData.value" :key="index">
|
||||
<view v-if="item.componentName == 'GoodsCategory'">
|
||||
<diy-category @tologin="toLogin" ref="category" :value="item"></diy-category>
|
||||
@@ -16,8 +15,8 @@
|
||||
<!-- 隐私弹窗 -->
|
||||
<privacy-popup ref="privacyPopup"></privacy-popup>
|
||||
|
||||
<!-- 底部tabBar: loading 期间隐藏, 等 loading-cover 关闭后下一 tick 再挂载, 避免 TabBar 与 loading-cover 同时 GPU 合成导致频闪 -->
|
||||
<view id="tab-bar" v-if="!loadingVisible">
|
||||
<!-- 底部tabBar -->
|
||||
<view id="tab-bar">
|
||||
<diy-bottom-nav></diy-bottom-nav>
|
||||
</view>
|
||||
</view>
|
||||
@@ -28,13 +27,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
diyData: { value: [] },
|
||||
mpShareData: null, //小程序分享数据
|
||||
// 修复底部 TabBar 频闪: 用本地 loadingVisible 状态镜像 <loading-cover> 的可见性,
|
||||
// loading-cover 的 spin 动画与 diy-bottom-nav 都是 fixed/z-index 高 + GPU 合成,
|
||||
// 在 MP-Weixin 下会互相触发重绘, 让用户看到 TabBar 抖动。
|
||||
// 策略: loading-cover 显示期间, 也把 tabBar 先收起来, loading-cover 隐藏后
|
||||
// 下一次 nextTick 再放出 tabBar, 把整段 loading 过程的合成层冲突降到最低。
|
||||
loadingVisible: true
|
||||
mpShareData: null //小程序分享数据
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
@@ -47,13 +40,8 @@ export default {
|
||||
this.$util.hideHomeButton();
|
||||
try {
|
||||
// 鸿蒙元服务 JSVM 下 $refs.category 可能为 undefined, 需防御
|
||||
// 节流: 避免 onShow 重复触发 pageShow() 中的 getHeightArea(-1) 高耗时选择器查询, 引起频闪和性能损耗
|
||||
const now = Date.now();
|
||||
if (this._lastPageShowTime && now - this._lastPageShowTime < 500) {
|
||||
// 短时间内重复触发, 跳过本次 pageShow
|
||||
} else if (this.$refs.category && this.$refs.category[0]) {
|
||||
if (this.$refs.category && this.$refs.category[0]) {
|
||||
this.$refs.category[0].pageShow();
|
||||
this._lastPageShowTime = now;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('onShow pageShow failed', e);
|
||||
@@ -121,13 +109,6 @@ export default {
|
||||
this._diyInfoLoading = false;
|
||||
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
|
||||
uni.stopPullDownRefresh();
|
||||
// loading-cover 收起后, 等下一 tick 让出 frame, 再挂载 diy-bottom-nav
|
||||
// (避免两个 fixed 高 z-index GPU 合成层在同帧 mount 触发 MP-Weixin 重绘频闪)
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
this.loadingVisible = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<template>
|
||||
<!-- 鸿蒙兼容: themeColor 是 CSS 变量字符串, 鸿蒙 JSVM 早期版本对 :style="字符串" 处理可能不稳.
|
||||
兜底: 只有非空字符串才绑定 :style. -->
|
||||
<view :style="themeColor || undefined">
|
||||
<view :style="themeColor">
|
||||
<!-- #ifndef H5 -->
|
||||
<!-- <view class="page-header" v-if="goodsSkuDetail && goodsSkuDetail.config && goodsSkuDetail.config.nav_bar_switch == 0">
|
||||
<ns-navbar :data="navbarData" :isBack="!isTabRootPage"></ns-navbar>
|
||||
</view> -->
|
||||
<!-- #endif -->
|
||||
|
||||
<goods-detail-view :goodsSkuDetail="goodsSkuDetail" :loading="loading" ref="goodsDetailView" @aiChatClick="sendToAIChat(goodsSkuDetail)">
|
||||
<goods-detail-view :goodsSkuDetail="goodsSkuDetail" ref="goodsDetailView" @aiChatClick="sendToAIChat(goodsSkuDetail)">
|
||||
<!-- 价格区域 -->
|
||||
<template v-slot:price>
|
||||
<!-- 限时折扣 -->
|
||||
@@ -74,9 +72,7 @@
|
||||
</view>
|
||||
<view class="follow-and-share">
|
||||
<text class="follow iconfont icon-share" @click="openSharePopup()"></text>
|
||||
<!-- #ifndef MP-HARMONY -->
|
||||
<text class="share iconfont" @click="editCollection()" :class="whetherCollection == 1 ? 'icon-likefill color-base-text' : 'icon-guanzhu'"></text>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</block>
|
||||
<block v-else>
|
||||
@@ -415,12 +411,7 @@
|
||||
</view> -->
|
||||
<!-- v-if="goodsSkuDetail.goodsForm" -->
|
||||
<view style="padding: 0 30rpx;">
|
||||
<!-- 表单暂无字段时的友好提示, 避免"打开弹窗却什么都不显示"困惑用户 -->
|
||||
<view v-if="!formDiyValue || !formDiyValue.jsonData || !formDiyValue.jsonData.length" class="form-empty" style="padding: 60rpx 0; text-align: center; color: #999;">
|
||||
{{ $lang('goods.detail.formEmptyTip') }}
|
||||
</view>
|
||||
<diy-form
|
||||
v-else
|
||||
:value="formDiyValue"
|
||||
dataMode="raw"
|
||||
:show-submit="false"
|
||||
@@ -459,7 +450,7 @@
|
||||
/>
|
||||
<block v-else>
|
||||
<ns-goods-action-icon :text="$lang('goods.detail.home')" icon="icon-shouye1" @click="goHome" />
|
||||
|
||||
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<ns-goods-action-icon
|
||||
:text="$lang('goods.detail.cart')"
|
||||
@@ -467,12 +458,9 @@
|
||||
icon="icon-gouwuche2"
|
||||
:corner-mark="cartNumber > 0 ? cartNumber + '' : ''"
|
||||
@click="goCart"
|
||||
/>
|
||||
/>
|
||||
<ns-goods-action-icon :text="$lang('goods.detail.kefu')" icon="icox icox-kefu" :send-data="contactData" :chatParam="chatRoomParams" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<!-- 鸿蒙平台: 隐藏购物车、客服、收藏按钮 -->
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef H5 -->
|
||||
<ns-goods-action-icon :text="$lang('goods.detail.follow')" :icon="whetherCollection == 1 ? 'icon-likefill color-base-text' : 'icon-guanzhu'" @click="editCollection" />
|
||||
<!-- #endif -->
|
||||
@@ -528,9 +516,7 @@
|
||||
</goods-detail-view>
|
||||
<to-top v-if="showTop" @toTop="scrollToTopNative()"></to-top>
|
||||
<ns-login ref="login"></ns-login>
|
||||
<!-- 商品详情页已有骨架屏, 不再启用 loading-cover 全屏遮罩(initShow=false),
|
||||
否则骨架屏会被遮罩盖住、用户看不到任何占位过渡 -->
|
||||
<loading-cover ref="loadingCover" :initShow="false"></loading-cover>
|
||||
<loading-cover ref="loadingCover"></loading-cover>
|
||||
|
||||
<!-- 隐私弹窗 -->
|
||||
<privacy-popup ref="privacyPopup"></privacy-popup>
|
||||
|
||||
@@ -40,9 +40,7 @@ export default {
|
||||
this.isAction = false;
|
||||
},
|
||||
onUnload() {
|
||||
if (!this.storeToken && this.$refs.login && typeof this.$refs.login.cancelCompleteInfo === 'function') {
|
||||
this.$refs.login.cancelCompleteInfo();
|
||||
}
|
||||
if (!this.storeToken && this.$refs.login) this.$refs.login.cancelCompleteInfo();
|
||||
},
|
||||
onReady() {
|
||||
if (!this.storeToken) {
|
||||
|
||||
@@ -4,10 +4,7 @@ export default {
|
||||
goodsRoute: '/pages_goods/detail',
|
||||
couponList: [], //优惠券列表
|
||||
couponBtnSwitch: false, //获取优惠券防止重复提交
|
||||
// 商品详情骨架屏:true 显示骨架,false 显示真实内容
|
||||
// 首屏进入即为 true,商品详情数据首屏渲染完成后(与 loading-cover 同时机)置 false
|
||||
loading: true,
|
||||
|
||||
|
||||
posterApi: '/api/goods/poster',
|
||||
|
||||
//满减活动
|
||||
@@ -56,19 +53,11 @@ export default {
|
||||
}
|
||||
return discount.toFixed(2);
|
||||
},
|
||||
// 弹窗高度: 受限于视口, 内部表单 <scroll-view> 自带滚动, 不再按字段数无限加高
|
||||
FormHeight() {
|
||||
// 上限 65vh: 留足顶部状态栏/导航栏 + 底部 home indicator, 也不会显得"一打开就半屏遮脸"
|
||||
// 字段多时通过内部 <scroll-view scroll-y> 自然滚动
|
||||
const formList = (this.goodsSkuDetail && this.goodsSkuDetail.goods_form)
|
||||
|| this.goodsForm;
|
||||
let height = 45;
|
||||
if (formList && formList.length) {
|
||||
// 少量字段: 每个约 5vh; 字段多时递增放缓并封顶, 避免接近屏顶
|
||||
// 5 个字段 → 45 + min(10, 25) = 55vh
|
||||
// 10 个字段 → 45 + min(10, 50) = 55vh (已封顶)
|
||||
const dynamic = Math.min(10, formList.length * 5);
|
||||
height = Math.min(55, height + dynamic);
|
||||
//表单高度
|
||||
FormHeight(){
|
||||
let height = 48;
|
||||
if (this.goodsForm && this.goodsForm.length) {
|
||||
height += this.goodsForm.length * 5;
|
||||
}
|
||||
height += 'vh';
|
||||
return height;
|
||||
@@ -100,19 +89,8 @@ export default {
|
||||
// #endif
|
||||
},
|
||||
async onShow() {
|
||||
// 同步获取商品详情
|
||||
// 鸿蒙元服务兼容: 包 try/catch, 防止内部任何抛错导致整个 page 显示空白
|
||||
try {
|
||||
await this.getGoodsSkuDetail();
|
||||
} catch (e) {
|
||||
console.error('getGoodsSkuDetail failed:', e);
|
||||
// 失败时不关闭骨架屏, 由视图层 hasRealData(goods_id>0) 自动保留占位,
|
||||
// 避免错误地把页面切到"空详情"状态
|
||||
uni.showToast({
|
||||
title: this.$lang ? this.$lang('common.networkError') : '网络异常,请重试',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
//同步获取商品详情
|
||||
await this.getGoodsSkuDetail();
|
||||
},
|
||||
onHide() {
|
||||
this.couponBtnSwitch = false;
|
||||
@@ -134,28 +112,14 @@ export default {
|
||||
},
|
||||
// 获取商品详情
|
||||
async getGoodsSkuDetail() {
|
||||
let res;
|
||||
try {
|
||||
// 修复鸿蒙元服务: 必须传 async: false !
|
||||
// 当 App.vue 的 boot (lang/theme/remote-config) 未完成时,
|
||||
// sendRequest 在 async !== false 模式下会直接 return undefined (不返回 promise),
|
||||
// 导致 res.code undefined / res.data 抛 TypeError, 整个 onShow 失败.
|
||||
res = await this.$api.sendRequest({
|
||||
url: '/api/goodssku/detail',
|
||||
async: false,
|
||||
data: {
|
||||
sku_id: this.skuId,
|
||||
goods_id: this.goodsId,
|
||||
}
|
||||
});
|
||||
// 防御: 即便 async:false 仍可能在极端情况下返回 undefined
|
||||
if (!res || typeof res !== 'object') {
|
||||
throw new Error('sendRequest returned non-object response (likely boot not complete)');
|
||||
let res = await this.$api.sendRequest({
|
||||
url: '/api/goodssku/detail',
|
||||
async: false,
|
||||
data: {
|
||||
sku_id: this.skuId,
|
||||
goods_id: this.goodsId,
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('getGoodsSkuDetail sendRequest failed:', e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
let data = res.data;
|
||||
if (data.goods_sku_detail != null) {
|
||||
if (data.goods_sku_detail.promotion_type == 'presale' && data.goods_sku_detail.presale_id) {
|
||||
@@ -166,10 +130,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
if(uni.getStorageSync('lang') == 'en-us') data.goods_sku_detail.goods_name = data.goods_sku_detail.en_goods_name
|
||||
|
||||
// 关键改动: 先一次性赋值, 释放事件循环让 MP-Weixin 把首屏 (含媒体/价格/标题) 渲出来,
|
||||
// 再 $nextTick 后才跑同步字段变换 (sku_images split / JSON.parse / 属性去重 等),
|
||||
// 避免连续多次 Vue 响应式写触发同一帧内多次 vdom 重算, 显著改善"打开详情很慢"的体感。
|
||||
|
||||
this.goodsSkuDetail = data.goods_sku_detail;
|
||||
if (!this.skuId) this.skuId = this.goodsSkuDetail.sku_id;
|
||||
if (!this.goodsId) this.goodsId = this.goodsSkuDetail.goods_id;
|
||||
@@ -181,10 +142,7 @@ export default {
|
||||
this.chatRoomParams = {
|
||||
sku_id: this.goodsSkuDetail.sku_id
|
||||
};
|
||||
// 鸿蒙兼容: 后端在部分场景下可能不返回 goods_promotion, 缺字段直接 .[0] 会抛 TypeError,
|
||||
// 整个 onShow 走 catch 路径无法关闭骨架屏, 表现为"数据已回来但永远显示骨架屏"
|
||||
const promotionList = this.goodsSkuDetail?.goods_promotion || [];
|
||||
let typeId = promotionList.length > 0 ? promotionList[0] : undefined;
|
||||
let typeId = this.goodsSkuDetail.goods_promotion[0];
|
||||
if (typeId) {
|
||||
// 限时折扣
|
||||
if (typeId.discount_id) {
|
||||
@@ -198,27 +156,7 @@ export default {
|
||||
goods_id: this.goodsId
|
||||
};
|
||||
|
||||
// 让首屏主区域先绘制
|
||||
// 兼容鸿蒙元服务: $nextTick 在某些 JSVM 实现下不可靠, 用 setTimeout 兜底
|
||||
// #ifdef MP-HARMONY
|
||||
await new Promise(resolve => setTimeout(resolve, 16));
|
||||
// 修复鸿蒙元服务: 不依赖 setTimeout 关闭骨架屏.
|
||||
// 鸿蒙 JSVM 在 onShow 早期窗口的 setTimeout 回调有时被推迟/丢弃, 表现为"数据已回来但永远显示骨架屏".
|
||||
// 数据已赋值 + 首屏让步, 同步直接关闭骨架屏, 由 hasRealData 兜底避免空详情.
|
||||
this.loading = false;
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-HARMONY
|
||||
await this.$nextTick();
|
||||
// 关闭商品详情骨架屏: 在真实数据首屏渲染完成后再隐藏骨架屏,
|
||||
// 用户会看到骨架屏 → 真实详情 的清晰过渡
|
||||
// 最短展示 300ms: 数据请求很快时也保留足够感知时间, 否则一闪而过用户根本看不到骨架屏
|
||||
setTimeout(() => {
|
||||
this.loading = false;
|
||||
}, 300);
|
||||
// #endif
|
||||
|
||||
// 处理商品数据 (同步字段变换 + 后台并发 getGoodsForm)
|
||||
// 处理商品数据
|
||||
await this.handleGoodsSkuData();
|
||||
|
||||
// 限时折扣
|
||||
@@ -255,7 +193,7 @@ export default {
|
||||
}
|
||||
|
||||
// 优惠券
|
||||
if (Array.isArray(this.goodsSkuDetail.coupon_list)) {
|
||||
if (this.goodsSkuDetail.coupon_list) {
|
||||
this.couponList = this.goodsSkuDetail.coupon_list;
|
||||
this.couponList.forEach(v => {
|
||||
if (v.count == v.lead_count) v.useState = 2;
|
||||
@@ -270,6 +208,8 @@ export default {
|
||||
// this.handleBundlingData(this.goodsSkuDetail.bundling_list);
|
||||
// }
|
||||
|
||||
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
|
||||
|
||||
} else {
|
||||
this.$util.redirectTo('/pages_tool/goods/not_exist', {}, 'redirectTo');
|
||||
}
|
||||
@@ -281,35 +221,8 @@ export default {
|
||||
},
|
||||
|
||||
//商品留言打开
|
||||
async showinformation(){
|
||||
// 修复: 第一次打开时, uni-popup 内部 <scroll-view><diy-form> 还没完成首次渲染,
|
||||
// 直接 open() 会让用户看到空白弹窗。这里:
|
||||
// 1) 若商品表单后台请求尚未完成 (或根本没触发), 先发起并 await 该请求
|
||||
// 2) 用 $nextTick 等组件挂载完成后再打开
|
||||
try {
|
||||
// 若页面已进入但 _goodsFormPromise 还没建 (如 sku 切换/数据未到等场景),
|
||||
// 这里补一个并发请求; 若已存在, 复用旧 promise 不重复请求。
|
||||
if (!this._goodsFormPromise || typeof this._goodsFormPromise.then !== 'function') {
|
||||
const loader = (this.getGoodsForm && typeof this.getGoodsForm === 'function')
|
||||
? this.getGoodsForm()
|
||||
: Promise.resolve();
|
||||
this._goodsFormPromise = loader;
|
||||
await loader;
|
||||
} else {
|
||||
await this._goodsFormPromise;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('await goods form promise failed', e);
|
||||
}
|
||||
// 第一次强制刷新 popup 内部组件布局 (nextTick), 确保表单完整渲染
|
||||
// 兼容鸿蒙元服务: $nextTick 不可靠时降级到 setTimeout
|
||||
try { await this.$nextTick(); } catch (e) { /* 兜底 */ }
|
||||
// 微信小程序下 scroll-view 在首次以 transform 隐藏后再显示时, 内部子节点布局可能未刷新
|
||||
// 双 nextTick 兜底, 保证表单完整可见
|
||||
try { await this.$nextTick(); } catch (e) { /* 兜底 */ }
|
||||
if (this.$refs.informationPopup && typeof this.$refs.informationPopup.open === 'function') {
|
||||
this.$refs.informationPopup.open();
|
||||
}
|
||||
showinformation(){
|
||||
this.$refs.informationPopup.open();
|
||||
},
|
||||
//商品留言关闭
|
||||
closeinformationPopup(){
|
||||
|
||||
@@ -37,7 +37,7 @@ export default {
|
||||
fontWeight: false,
|
||||
padding: 0,
|
||||
cartEvent: "detail",
|
||||
text: "",
|
||||
text: "购买",
|
||||
textColor: "#FFFFFF",
|
||||
theme: "default",
|
||||
aroundRadius: 25,
|
||||
@@ -67,7 +67,7 @@ export default {
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this.config.text = this.$lang('goods.list.buyText') || this.$lang('buyNow');
|
||||
this.config.text = this.$lang('goods.list.buyText');
|
||||
|
||||
this.$api.sendRequest({
|
||||
url: '/api/lucky/status',
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
<view class="order-cell coupon" v-if="modules.indexOf('coupon') != -1">
|
||||
<text class="tit">{{ $lang('order.payment.orderMoney.coupon') }}</text>
|
||||
<view class="box text-overflow"@click="openSiteCoupon(calculateGoodsData.merch_id)">
|
||||
<template v-if="(orderCreateData.coupon && orderCreateData.coupon[calculateGoodsData.merch_id] && orderCreateData.coupon[calculateGoodsData.merch_id].coupon_id)">
|
||||
<template v-if="orderCreateData.coupon[calculateGoodsData.merch_id].coupon_id">
|
||||
<text class="money price-font" style="max-width: 100%;">{{ $lang('order.payment.orderMoney.couponSelected', { count: 1 }) }}</text>
|
||||
<!-- <text class="unit price-font">优惠</text>
|
||||
<text class="money price-font">{{ (calculateData && calculateData.coupon_money ? calculateData.coupon_money : 0) | moneyFormat }}</text> -->
|
||||
@@ -550,7 +550,7 @@
|
||||
<view v-if="couponItem.type == 'discount' && couponItem.discount_limit > 0" class="limit">{{ $lang('order.payment.couponPopup.maxDiscount', { amount: couponItem.discount_limit }) }}</view>
|
||||
<view class="time font-size-goods-tag">{{ $lang('order.payment.couponPopup.validPeriod') }}{{ couponItem.end_time ? $util.timeStampTurnTime(couponItem.end_time) : $lang('order.payment.couponPopup.permanentValid') }}</view>
|
||||
</view>
|
||||
<view class="iconfont" :class="selectCouponMap[merchCoupon.merch_id] == couponItem.coupon_id ? 'icon-yuan_checked color-base-text' : 'icon-yuan_checkbox'"></view>
|
||||
<view class="iconfont" :class="selectCouponId == couponItem.coupon_id ? 'icon-yuan_checked color-base-text' : 'icon-yuan_checkbox'"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -55,8 +55,7 @@ export default {
|
||||
merch_id: 0,
|
||||
data: []
|
||||
},
|
||||
// 按 merch_id 隔离,避免多商户之间互相污染
|
||||
selectCouponMap: {}
|
||||
selectCouponId:0
|
||||
}
|
||||
},
|
||||
inject: ['promotion'],
|
||||
@@ -86,7 +85,7 @@ export default {
|
||||
goodsData() {
|
||||
if (this.paymentData) {
|
||||
this.paymentData.goods_list.forEach(item => {
|
||||
if (item.sku_spec_format && typeof item.sku_spec_format === 'string') item.sku_spec_format = JSON.parse(item.sku_spec_format);
|
||||
if (item.sku_spec_format) item.sku_spec_format = JSON.parse(item.sku_spec_format);
|
||||
})
|
||||
return this.paymentData;
|
||||
}
|
||||
@@ -94,7 +93,7 @@ export default {
|
||||
calculateGoodsData() {
|
||||
if (this.calculateData) {
|
||||
this.calculateData.goods_list.forEach(item => {
|
||||
if (item.sku_spec_format && typeof item.sku_spec_format === 'string') item.sku_spec_format = JSON.parse(item.sku_spec_format);
|
||||
if (item.sku_spec_format) item.sku_spec_format = JSON.parse(item.sku_spec_format);
|
||||
})
|
||||
return this.calculateData;
|
||||
}
|
||||
@@ -190,7 +189,7 @@ export default {
|
||||
//多商户商品分类
|
||||
goodsSpecFormat(goods_list){
|
||||
goods_list.forEach(item => {
|
||||
if (item.sku_spec_format && typeof item.sku_spec_format === 'string') item.sku_spec_format = JSON.parse(item.sku_spec_format);
|
||||
if (item.sku_spec_format) item.sku_spec_format = JSON.parse(item.sku_spec_format);
|
||||
})
|
||||
return goods_list
|
||||
},
|
||||
@@ -250,11 +249,7 @@ export default {
|
||||
this.modules = data.modules;
|
||||
|
||||
//记录多商户优惠券
|
||||
// 兜底为对象,避免后续被当作数组处理
|
||||
if (!data.coupon || typeof data.coupon !== 'object' || Array.isArray(data.coupon)) {
|
||||
data.coupon = {};
|
||||
}
|
||||
this.orderCreateData.coupon = data.coupon;
|
||||
this.orderCreateData.coupon = data.coupon
|
||||
|
||||
//查询优惠券
|
||||
this.getCouponList();
|
||||
@@ -370,17 +365,7 @@ export default {
|
||||
}
|
||||
});
|
||||
//多商户
|
||||
// 保留用户已选的 coupon_id,后端回传的 res.data.coupon 默认都是 0 会覆盖掉
|
||||
const prevCoupon = (this.orderCreateData.coupon && typeof this.orderCreateData.coupon === 'object' && !Array.isArray(this.orderCreateData.coupon)) ? this.orderCreateData.coupon : {};
|
||||
this.orderCreateData.coupon = res.data.coupon || {};
|
||||
if (this.orderCreateData.coupon && typeof this.orderCreateData.coupon === 'object' && !Array.isArray(this.orderCreateData.coupon)) {
|
||||
Object.keys(prevCoupon).forEach(merch_id => {
|
||||
const chosen = prevCoupon[merch_id] && prevCoupon[merch_id].coupon_id;
|
||||
if (chosen && (!this.orderCreateData.coupon[merch_id] || !this.orderCreateData.coupon[merch_id].coupon_id)) {
|
||||
this.orderCreateData.coupon[merch_id] = { coupon_id: chosen };
|
||||
}
|
||||
});
|
||||
}
|
||||
this.orderCreateData.coupon = res.data.coupon
|
||||
// if (!res.data.coupon_id) this.orderCreateData.coupon.coupon_id = 0;
|
||||
// else this.orderCreateData.coupon.coupon_id = res.data.coupon_id;
|
||||
|
||||
@@ -746,52 +731,44 @@ export default {
|
||||
this.tempData = null;
|
||||
},
|
||||
//优惠券单独多商户
|
||||
/**
|
||||
* 显示店铺优惠券信息
|
||||
* @param {Object} siteId
|
||||
* @param {Object} couponData
|
||||
*/
|
||||
openSiteCoupon(merch_id) {
|
||||
|
||||
this.tempData = this.$util.deepClone(this.orderCreateData);
|
||||
this.merchCoupon.merch_id = merch_id;
|
||||
// 从当前订单已选的 coupon_id 取值,没有则取弹窗里缓存的,再没有为 0
|
||||
const exist = this.orderCreateData.coupon && this.orderCreateData.coupon[merch_id];
|
||||
const cached = this.selectCouponMap[merch_id] || 0;
|
||||
this.selectCouponMap[merch_id] = exist ? (exist.coupon_id || 0) : cached;
|
||||
// this.selectCouponMoney = this.orderCreateData.coupon[siteId].coupon_money;
|
||||
|
||||
// 兜底: 列表为空时给一个空数组,弹窗不会渲染空指针
|
||||
this.merchCoupon.data = (this.coupon_list && this.coupon_list[merch_id]) || [];
|
||||
this.$refs.couponPopup.open();
|
||||
this.$forceUpdate();
|
||||
},
|
||||
/**商户选择券*/
|
||||
selectCoupon(data, merch_id) {
|
||||
|
||||
// if (this.orderCreateData.coupon[merch_id].coupon_id == data.coupon_id){
|
||||
// this.orderCreateData.coupon[merch_id].coupon_id = 0
|
||||
// }else{
|
||||
// this.orderCreateData.coupon[merch_id].coupon_id = data.coupon_id
|
||||
// };
|
||||
const cur = this.selectCouponMap[merch_id] || 0;
|
||||
this.selectCouponMap[merch_id] = (cur != data.coupon_id) ? data.coupon_id : 0;
|
||||
this.$forceUpdate();
|
||||
// this.orderCreateData.coupon[merch_id].coupon_id = data.coupon_id
|
||||
},
|
||||
//确定使用优惠券
|
||||
useCpopon() {
|
||||
const merch_id = this.merchCoupon.merch_id;
|
||||
// 确保 orderCreateData.coupon 是对象结构,后端按 merch_id 取值
|
||||
if (!this.orderCreateData.coupon || typeof this.orderCreateData.coupon !== 'object' || Array.isArray(this.orderCreateData.coupon)) {
|
||||
this.orderCreateData.coupon = {};
|
||||
}
|
||||
this.orderCreateData.coupon[merch_id] = {
|
||||
coupon_id: this.selectCouponMap[merch_id] || 0
|
||||
};
|
||||
this.$refs.couponPopup.close();
|
||||
this.calculate();
|
||||
},
|
||||
/**
|
||||
* 显示店铺优惠券信息
|
||||
* @param {Object} siteId
|
||||
* @param {Object} couponData
|
||||
*/
|
||||
openSiteCoupon(merch_id) {
|
||||
|
||||
this.tempData = this.$util.deepClone(this.orderCreateData);
|
||||
this.merchCoupon.merch_id = merch_id;
|
||||
this.selectCouponId = this.orderCreateData.coupon[merch_id].coupon_id;
|
||||
// this.selectCouponMoney = this.orderCreateData.coupon[siteId].coupon_money;
|
||||
|
||||
this.merchCoupon.data = this.coupon_list[merch_id];
|
||||
this.$refs.couponPopup.open();
|
||||
this.$forceUpdate();
|
||||
},
|
||||
/**商户选择券*/
|
||||
selectCoupon(data,merch_id) {
|
||||
|
||||
// if (this.orderCreateData.coupon[merch_id].coupon_id == data.coupon_id){
|
||||
// this.orderCreateData.coupon[merch_id].coupon_id = 0
|
||||
// }else{
|
||||
// this.orderCreateData.coupon[merch_id].coupon_id = data.coupon_id
|
||||
// };
|
||||
if (this.selectCouponId != data.coupon_id) {
|
||||
this.selectCouponId = data.coupon_id;
|
||||
} else {
|
||||
this.selectCouponId = 0;
|
||||
}
|
||||
this.$forceUpdate();
|
||||
// this.orderCreateData.coupon[merch_id].coupon_id = data.coupon_id
|
||||
},
|
||||
//确定使用优惠券
|
||||
useCpopon() {
|
||||
this.orderCreateData.coupon[this.merchCoupon.merch_id].coupon_id = this.selectCouponId;
|
||||
this.$refs.couponPopup.close();
|
||||
this.calculate();
|
||||
},
|
||||
|
||||
//-------------------
|
||||
/**
|
||||
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
fontWeight: false,
|
||||
padding: 0,
|
||||
cartEvent: "detail",
|
||||
text: '',
|
||||
text: this.$lang('merch.list.buyText'),
|
||||
textColor: "#FFFFFF",
|
||||
theme: "default",
|
||||
aroundRadius: 25,
|
||||
@@ -66,7 +66,6 @@ export default {
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this.config.text = this.$lang('merch.list.buyText');
|
||||
this.categoryId = options.category_id || 0;
|
||||
this.keyword = options.keyword || '';
|
||||
this.coupon = options.coupon || 0;
|
||||
|
||||
@@ -397,7 +397,7 @@ export default {
|
||||
scrollTop: 0,
|
||||
scrollIntoViewId: null,
|
||||
hasMoreHistory: false,
|
||||
loadingText: '',
|
||||
loadingText: this.$lang('ai-chat.index.loadMoreHistory'),
|
||||
voiceInputing: false,
|
||||
audioContext: null,
|
||||
currentAudio: null,
|
||||
@@ -417,7 +417,7 @@ export default {
|
||||
localUserAvatar: '',
|
||||
showAvatarSelector: false, // 头像选择弹窗控制
|
||||
localUserNickname: '', // 当前用户昵称
|
||||
defaultNickname: '', // 默认昵称
|
||||
defaultNickname: this.$lang('ai-chat.index.defaultNickname'), // 默认昵称
|
||||
showNicknameEditor: false, // 昵称编辑弹窗控制
|
||||
// AI头像错误状态
|
||||
avatarError: false,
|
||||
@@ -493,9 +493,6 @@ export default {
|
||||
created() {
|
||||
this.messages = [...this.initialMessages]
|
||||
this.messageId = this.messages.length
|
||||
// 初始化 i18n 字段(在 created 中同步赋值,避免 remote-only 首屏 data() 缓存空串)
|
||||
this.loadingText = this.$lang('ai-chat.index.loadMoreHistory')
|
||||
this.defaultNickname = this.$lang('ai-chat.index.defaultNickname')
|
||||
this.initAudioContext()
|
||||
// 初始化用户头像(缓存优先)
|
||||
this.initUserAvatarData()
|
||||
|
||||
@@ -29,8 +29,7 @@
|
||||
detail: null,
|
||||
isRepeat: false,
|
||||
complete: false,
|
||||
scroll: true,
|
||||
urlParams: {}
|
||||
scroll:true
|
||||
};
|
||||
},
|
||||
onLoad(data) {
|
||||
@@ -40,27 +39,12 @@
|
||||
// #endif
|
||||
|
||||
this.id = data.id || 0;
|
||||
|
||||
// 通用 URL 参数收集:除 id/scene 外的所有参数都透传给后端
|
||||
// 后端会做卫生过滤并广播事件,供 recommend 等插件消费
|
||||
const reservedKeys = ['uniacid', 'id', 'scene'];
|
||||
this.urlParams = {};
|
||||
for (let key in data) {
|
||||
if (reservedKeys.indexOf(key) !== -1) continue;
|
||||
this.urlParams[key] = data[key];
|
||||
}
|
||||
|
||||
if (data.scene) {
|
||||
var sceneParams = decodeURIComponent(data.scene);
|
||||
sceneParams = sceneParams.split('&');
|
||||
if (sceneParams.length) {
|
||||
sceneParams.forEach(item => {
|
||||
if (item.indexOf('id') != -1) this.id = item.split('-')[1];
|
||||
// scene 中也可能携带其他参数
|
||||
let pair = item.split('=');
|
||||
if (pair.length === 2 && pair[0] !== 'id') {
|
||||
this.urlParams[pair[0]] = pair[1];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -105,9 +89,7 @@
|
||||
url: '/form/api/form/create',
|
||||
data: {
|
||||
form_id: this.id,
|
||||
form_data: typeof this.$refs.form.formData === 'string' ? this.$refs.form.formData : JSON.stringify(this.$refs.form.formData),
|
||||
url_params: JSON.stringify(this.urlParams),
|
||||
member_id: this.memberInfo ? this.memberInfo.id : 0
|
||||
form_data: typeof this.$refs.form.formData === 'string' ? this.$refs.form.formData : JSON.stringify(this.$refs.form.formData)
|
||||
},
|
||||
success: res => {
|
||||
if (res.code == 0) {
|
||||
@@ -180,4 +162,4 @@
|
||||
bottom: 180rpx !important;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -1,41 +1,21 @@
|
||||
<template>
|
||||
<view :style="themeColor">
|
||||
<!--
|
||||
自绘导航栏(仅在 navigationStyle:custom 的端渲染:APP / 微信小程序 / 华为快应用 / 鸿蒙元服务)
|
||||
- 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>
|
||||
<!-- #endif -->
|
||||
<block v-if="detail">
|
||||
<view class="page">
|
||||
<view class="system-form-wrap">
|
||||
<!-- 复用 diy-form:banner / tip / 表单字段 / 提交按钮 全部由其内部渲染,样式(bannerPaddingX/tipColor/submitBgColor 等)由 form_config 控制。
|
||||
:onSubmit 接管提交逻辑(带 uniacid + showModal);跨 H5 / MP 一致,不依赖 $listeners。
|
||||
注意:create 重度依赖本组件 this(id/uniacid/memberInfo/$api 等),必须在 created 阶段 bind this,
|
||||
否则 diy-form 通过 this.onSubmit(formData) 调用时 this 会漂移到子组件实例 -->
|
||||
<diy-form ref="form" :value="diyValue" dataMode="raw" :onSubmit="handleSubmit" />
|
||||
@submit 接管提交逻辑(带 uniacid + showModal) -->
|
||||
<diy-form
|
||||
ref="form"
|
||||
:value="diyValue"
|
||||
dataMode="raw"
|
||||
@submit="create"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<block v-else>
|
||||
<ns-empty :text="complete ? $lang('form.formdata.submitSuccess') : $lang('form.formdata.noFormInfo')"
|
||||
:isIndex="false"></ns-empty>
|
||||
<ns-empty :text="complete ? $lang('form.formdata.submitSuccess') : $lang('form.formdata.noFormInfo')" :isIndex="false"></ns-empty>
|
||||
</block>
|
||||
<loading-cover ref="loadingCover"></loading-cover>
|
||||
<ns-login ref="login"></ns-login>
|
||||
@@ -45,9 +25,13 @@
|
||||
|
||||
<script>
|
||||
|
||||
import diyForm from '@/components-diy/diy-form.vue';
|
||||
import scroll from '@/common/js/scroll-view.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
diyForm
|
||||
},
|
||||
mixins: [scroll],
|
||||
data() {
|
||||
return {
|
||||
@@ -59,16 +43,6 @@ export default {
|
||||
uniacid: 0,
|
||||
isFromMpWebView: false, // 是否是从微信小程序的WebView跳转过来的
|
||||
hideNavbar: false, // 查询参数中支持隐藏Navbar
|
||||
showHomeBtn: false, // 查询参数 show_home=1 时,在系统标题栏左侧显示 Home 按钮
|
||||
urlParams: {}, // 通用 URL 透传参数收集
|
||||
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
|
||||
formTitle: '', // 查询参数传入的Title
|
||||
@@ -76,30 +50,7 @@ export default {
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 预绑定 this:create 重度依赖本组件 this(id/uniacid/memberInfo/$api/$lang/$util 等)。
|
||||
// 若直接传 :onSubmit="create",diy-form 通过 this.onSubmit(formData) 调用时,
|
||||
// 普通函数调用的 this 会指向调用者(子组件实例),导致字段全部读不到。
|
||||
// bind 后无论子组件如何调用,this 始终是 formdata 实例。
|
||||
this.handleSubmit = this.create.bind(this);
|
||||
},
|
||||
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 形态:
|
||||
// - jsonData:透传给 ns-newform(已是 ns-newform 期望的 [{ controller, value, val }])
|
||||
// - formConfig:后端返回 camelCase(如 formPaddingX / formBgColor / labelFontSize),
|
||||
@@ -119,7 +70,6 @@ export default {
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
console.log("=====表单页面====", options)
|
||||
if (!options.uniacid) {
|
||||
uni.showModal({
|
||||
title: this.$lang('form.formdata.tip'),
|
||||
@@ -140,41 +90,7 @@ export default {
|
||||
this.uniacid = options.uniacid
|
||||
this.id = options.id || 0
|
||||
this.isFromMpWebView = options.from === 'mp-webview';
|
||||
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 参数收集:除业务保留字段外,其余全部透传给后端
|
||||
// 后端会做卫生过滤并广播事件,供 recommend 等插件消费
|
||||
const reservedKeys = ['uniacid', 'id', 'scene', 'from', 'hide_navbar', 'show_home', 'title', 'enable_set_title'];
|
||||
this.urlParams = {};
|
||||
for (let key in options) {
|
||||
if (reservedKeys.indexOf(key) !== -1) continue;
|
||||
this.urlParams[key] = options[key];
|
||||
}
|
||||
this.hideNavbar = Boolean(options?.hide_navbar);
|
||||
|
||||
if (options.scene) {
|
||||
const scene = decodeURIComponent(options.scene)
|
||||
@@ -183,11 +99,6 @@ export default {
|
||||
if (item.indexOf('id') != -1) {
|
||||
this.id = item.split('-')[1]
|
||||
}
|
||||
// scene 中也可能携带其他参数
|
||||
let pair = item.split('=');
|
||||
if (pair.length === 2 && pair[0] !== 'id') {
|
||||
this.urlParams[pair[0]] = pair[1];
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -202,7 +113,7 @@ export default {
|
||||
|
||||
this.getData()
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
storeToken(newVal, oldVal) {
|
||||
newVal && this.getData()
|
||||
@@ -212,20 +123,6 @@ export default {
|
||||
this.updateNavbarTitle();
|
||||
},
|
||||
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() {
|
||||
this.$api.sendRequest({
|
||||
url: '/form/api/form/info',
|
||||
@@ -253,37 +150,22 @@ export default {
|
||||
formName = this.formTitle;
|
||||
}
|
||||
// #endif
|
||||
// H5 端:系统原生 navbar —— uni.setNavigationBarTitle 改原生标题
|
||||
// #ifdef H5
|
||||
uni.setNavigationBarTitle({ title: formName });
|
||||
// #endif
|
||||
// 非 H5 端:custom 模式下没有原生 navbar,直接写 data.navbarTitle,
|
||||
// 由模板里的 .form-home-navbar__title 显示
|
||||
// #ifndef H5
|
||||
this.navbarTitle = formName;
|
||||
// #endif
|
||||
},
|
||||
// 由 diy-form 内置提交按钮触发:diy-form handleSubmit 已先做 verify(),
|
||||
// 仅在通过时才通过 :onSubmit 函数 prop 调用本方法(跨 H5 / MP 一致,不走 $emit / $listeners)。
|
||||
// 注意:本方法通过 created 中 this.handleSubmit = this.create.bind(this) 预绑定 this,
|
||||
// 因此内部所有 this.xxx 都指向 formdata 实例,不会漂移到子组件。
|
||||
// 由 diy-form 内置提交按钮触发:diy-form 已先做 verify(),仅在通过时才 emit('submit', formData)
|
||||
create(formData) {
|
||||
// 防重复提交:console 在拦截之前,避免日志噪声;拦截后立即置位
|
||||
if (this.isRepeat) return;
|
||||
if (this.isRepeat) return
|
||||
|
||||
this.isRepeat = true;
|
||||
this.isRepeat = true
|
||||
this.$api.sendRequest({
|
||||
url: '/form/api/form/create',
|
||||
data: {
|
||||
form_id: this.id,
|
||||
form_data: typeof formData === 'string' ? formData : JSON.stringify(formData),
|
||||
uniacid: this.uniacid,
|
||||
url_params: JSON.stringify(this.urlParams),
|
||||
member_id: this.memberInfo ? this.memberInfo.id : 0
|
||||
uniacid: this.uniacid
|
||||
},
|
||||
success: res => {
|
||||
if (res.code == 0) {
|
||||
this.complete = true
|
||||
uni.showModal({
|
||||
title: this.$lang('form.formdata.tip'),
|
||||
content: this.$lang('form.formdata.submitSuccess'),
|
||||
@@ -296,10 +178,6 @@ export default {
|
||||
title: res.message
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
// 网络失败时复位防重复标记,允许用户重试
|
||||
this.isRepeat = false
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -307,114 +185,6 @@ export default {
|
||||
}
|
||||
</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>
|
||||
page {
|
||||
background: #fff;
|
||||
|
||||
@@ -79,7 +79,7 @@ export default {
|
||||
setPublicShare() {
|
||||
let shareUrl = this.$config.h5Domain + '/pages_tool/help/list';
|
||||
this.$util.setPublicShare({
|
||||
title: this.$lang('help.list.title'),
|
||||
title: '帮助中心',
|
||||
desc: '',
|
||||
link: shareUrl,
|
||||
imgUrl: this.siteInfo ? this.$util.img(this.siteInfo.logo_square) : ''
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="body-wrap">
|
||||
<view class="form-wrap">
|
||||
<view class="form-wrap">
|
||||
<view class="input-wrap" v-show="loginMode == 'mobile'">
|
||||
<view class="content">
|
||||
<!-- <view class="area-code">+86</view> -->
|
||||
@@ -324,16 +324,16 @@ export default {
|
||||
}
|
||||
},
|
||||
mobileAuthLogin(e) {
|
||||
if (e.detail.errMsg == 'getPhoneNumber:ok') {
|
||||
var data = {
|
||||
iv: e.detail.iv,
|
||||
encryptedData: e.detail.encryptedData
|
||||
};
|
||||
if (Object.keys(this.authInfo).length) {
|
||||
Object.assign(data, this.authInfo);
|
||||
if (this.authInfo.nickName) data.nickname = this.authInfo.nickName;
|
||||
if (this.authInfo.avatarUrl) data.headimg = this.authInfo.avatarUrl;
|
||||
}
|
||||
if (e.detail.errMsg == 'getPhoneNumber:ok') {
|
||||
var data = {
|
||||
iv: e.detail.iv,
|
||||
encryptedData: e.detail.encryptedData
|
||||
};
|
||||
if (Object.keys(this.authInfo).length) {
|
||||
Object.assign(data, this.authInfo);
|
||||
if (this.authInfo.nickName) data.nickname = this.authInfo.nickName;
|
||||
if (this.authInfo.avatarUrl) data.headimg = this.authInfo.avatarUrl;
|
||||
}
|
||||
if (uni.getStorageSync('source_member')) data.source_member = uni.getStorageSync('source_member');
|
||||
|
||||
if (this.isSub) return;
|
||||
|
||||
@@ -95,10 +95,7 @@ export default {
|
||||
|
||||
// 判断登录
|
||||
if (!this.storeToken) {
|
||||
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁 + 跨平台分流:
|
||||
// 微信小程序 / 鸿蒙下跳首页 INDEX_PAGE_URL, 走 ns-login 原生授权弹窗;
|
||||
// H5 / APP 下跳原生登录页 LOGIN_PAGE_URL)
|
||||
this.redirectToLoginOnce();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
} else {
|
||||
this.getAccountInfo();
|
||||
}
|
||||
@@ -115,8 +112,8 @@ export default {
|
||||
this.fenxiao_info = res.data.fenxiao_info;
|
||||
}
|
||||
} else if (res.code == -1) {
|
||||
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁 + 跨平台分流)
|
||||
this.redirectToLoginOnce();
|
||||
this.clearLoginState();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,10 +20,7 @@
|
||||
|
||||
<script>
|
||||
import htmlParser from '@/common/js/html-parser';
|
||||
import memberPublic from './public/js/member-public.js';
|
||||
|
||||
export default {
|
||||
mixins: [memberPublic],
|
||||
data() {
|
||||
return {
|
||||
agreement: {},
|
||||
@@ -34,10 +31,7 @@ export default {
|
||||
if (option.back) this.back = option.back;
|
||||
// 判断登录
|
||||
if (!this.storeToken) {
|
||||
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁 + 跨平台分流:
|
||||
// 微信小程序 / 鸿蒙下跳首页 INDEX_PAGE_URL, 走 ns-login 原生授权弹窗;
|
||||
// H5 / APP 下跳原生登录页 LOGIN_PAGE_URL)
|
||||
this.redirectToLoginOnce();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
} else {
|
||||
this.getCancelAgreement();
|
||||
}
|
||||
@@ -51,8 +45,9 @@ export default {
|
||||
this.agreement = res.data;
|
||||
if (this.agreement.content) this.agreement.content = htmlParser(this.agreement.content);
|
||||
} else if (res.code == -1) {
|
||||
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁 + 跨平台分流)
|
||||
this.redirectToLoginOnce();
|
||||
this.$store.commit('setToken', '');
|
||||
this.$store.commit('setMemberInfo', '');
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import memberPublic from './public/js/member-public.js';
|
||||
|
||||
export default {
|
||||
mixins: [memberPublic],
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
@@ -29,8 +26,7 @@ export default {
|
||||
|
||||
// 判断登录
|
||||
if (!this.storeToken) {
|
||||
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁 + 跨平台分流)
|
||||
this.redirectToLoginOnce();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
} else {
|
||||
this.getStatus();
|
||||
}
|
||||
@@ -43,8 +39,9 @@ export default {
|
||||
if (res.code >= 0) {
|
||||
this.reason = res.data.reason;
|
||||
} else if (res.code == -1) {
|
||||
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁 + 跨平台分流)
|
||||
this.redirectToLoginOnce();
|
||||
this.$store.commit('setToken', '');
|
||||
this.$store.commit('setMemberInfo', '');
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,10 +49,7 @@
|
||||
onLoad(option) {
|
||||
// 判断登录
|
||||
if (!this.storeToken) {
|
||||
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁 + 跨平台分流:
|
||||
// 微信小程序 / 鸿蒙下跳首页 INDEX_PAGE_URL, 走 ns-login 原生授权弹窗;
|
||||
// H5 / APP 下跳原生登录页 LOGIN_PAGE_URL)
|
||||
this.redirectToLoginOnce();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
} else {
|
||||
this.getStatus();
|
||||
}
|
||||
@@ -73,8 +70,8 @@
|
||||
}
|
||||
|
||||
if (res.code == -1) {
|
||||
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁 + 跨平台分流)
|
||||
this.redirectToLoginOnce();
|
||||
this.clearLoginState();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,10 +19,7 @@
|
||||
components: {},
|
||||
data() {
|
||||
return {
|
||||
state: '',
|
||||
// 防抖锁: 防止 onLoad 的 getStatus 异步回调 和 用户点击 success 按钮
|
||||
// 短时间内连发 reLaunch, 触发微信小程序 "appLaunch with non-empty page stack" native 错误
|
||||
_navigated: false
|
||||
state: ''
|
||||
};
|
||||
},
|
||||
onLoad(option) {
|
||||
@@ -31,8 +28,7 @@
|
||||
|
||||
// 判断登录
|
||||
if (!this.storeToken) {
|
||||
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁)
|
||||
this.redirectToLoginOnce();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
} else {
|
||||
this.getStatus();
|
||||
}
|
||||
@@ -44,17 +40,16 @@
|
||||
success: res => {
|
||||
if (res.code >= 0) {
|
||||
if (res.data && res.data.status == 1) {
|
||||
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁)
|
||||
// 跟 success 按钮共享防抖, 避免同时触发
|
||||
this.redirectToLoginOnce();
|
||||
this.clearLoginState();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
success() {
|
||||
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁)
|
||||
this.redirectToLoginOnce();
|
||||
this.clearLoginState();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,32 +113,6 @@ export default {
|
||||
},
|
||||
onShow() {
|
||||
this.$util.hideHomeButton();
|
||||
},
|
||||
// 覆写 mixin 的分享钩子:优先使用推荐码组件存储的分享数据
|
||||
onShareAppMessage() {
|
||||
try {
|
||||
var shareDataStr = uni.getStorageSync('_recommendShareData');
|
||||
if (shareDataStr) {
|
||||
uni.removeStorageSync('_recommendShareData');
|
||||
return JSON.parse(shareDataStr);
|
||||
}
|
||||
} catch(e) {}
|
||||
return this.mpShareData?.appMessage;
|
||||
},
|
||||
onShareTimeline() {
|
||||
try {
|
||||
var shareDataStr = uni.getStorageSync('_recommendShareData');
|
||||
if (shareDataStr) {
|
||||
uni.removeStorageSync('_recommendShareData');
|
||||
var d = JSON.parse(shareDataStr);
|
||||
var query = '';
|
||||
if (d.path && d.path.indexOf('?') >= 0) {
|
||||
query = d.path.split('?')[1];
|
||||
}
|
||||
return { title: d.title, query: query, imageUrl: d.imageUrl || '' };
|
||||
}
|
||||
} catch(e) {}
|
||||
return this.mpShareData?.timeLine;
|
||||
},
|
||||
methods: {
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
methods: {
|
||||
// 清除登录态(token / 会员信息 / 购物车 / 本地存储)
|
||||
// 清除登录态(token / 用户信息 / 购物车 / 本地存储)
|
||||
clearLoginState() {
|
||||
this.$store.commit('setToken', '');
|
||||
this.$store.commit('setMemberInfo', '');
|
||||
@@ -9,48 +9,6 @@ export default {
|
||||
uni.removeStorageSync('privacyAgreed');
|
||||
},
|
||||
|
||||
// 跨平台登录入口:小程序下用 ns-login 原生授权弹窗 (走 wx.login / getPhoneNumber),
|
||||
// H5 / 快应用 / APP 下用原生登录页 /pages_tool/login/login (手机号+动态码表单).
|
||||
//
|
||||
// 修复背景: 之前所有跳转统一用 LOGIN_PAGE_URL (/pages_tool/login/login),
|
||||
// 这个原生登录页在微信小程序下是 form 提交 (像 H5), 没有走 wx.login 授权。
|
||||
// 期望行为:
|
||||
// - 微信小程序 / 鸿蒙元服务 / 其他小程序: 跳到首页 /pages/index/index,
|
||||
// 首页挂载了 <ns-login ref="login">, 用户后续操作触发 _requireHomeAuth
|
||||
// 时会自动弹 ns-login 弹窗, 走原生 wx.login / getPhoneNumber 授权.
|
||||
// - H5 / 快应用 / APP: 保持跳 LOGIN_PAGE_URL (原生登录页), 表单流程正常.
|
||||
getLoginEntryUrl() {
|
||||
// #ifdef MP-WEIXIN || MP-HARMONY || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-KUAISHOU || MP-JD || MP-360 || MP-XHS
|
||||
return this.$util.INDEX_PAGE_URL;
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN || MP-HARMONY || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-KUAISHOU || MP-JD || MP-360 || MP-XHS
|
||||
return this.$util.LOGIN_PAGE_URL;
|
||||
// #endif
|
||||
},
|
||||
|
||||
// 统一的"跳登录入口"封装, 包含组件级防抖锁 + 清登录态 + reLaunch
|
||||
//
|
||||
// 为什么需要防抖: 微信小程序下, 短时间内连发 reLaunch 会触发
|
||||
// "appLaunch with non-empty page stack" native 错误 (页面销毁未完成, 新 launch 时栈非空).
|
||||
// 注销/退出流程中常见并发场景:
|
||||
// - cancelsuccess.vue onLoad 调 getStatus 异步回调 + 用户点 "完成" 按钮
|
||||
// - 多个 submitCancelApply 同时被触发
|
||||
// 防抖策略: 同一组件实例 800ms 内只能跳一次, 之后忽略
|
||||
// (util.js reLaunch 分支还有 300ms 全局锁作为第二道防线)
|
||||
redirectToLoginOnce() {
|
||||
// 组件级防抖: 800ms 内重复调用直接忽略
|
||||
const now = Date.now();
|
||||
if (this._lastLoginRedirectAt && now - this._lastLoginRedirectAt < 800) {
|
||||
console.warn('[member-public] redirectToLoginOnce 防抖: 800ms 内重复调用已忽略');
|
||||
return;
|
||||
}
|
||||
this._lastLoginRedirectAt = now;
|
||||
this.clearLoginState();
|
||||
// 小程序下 getLoginEntryUrl() 返回首页 (走 ns-login 原生授权弹窗),
|
||||
// H5 / APP 下返回原生登录页 (form 提交)
|
||||
this.$util.redirectTo(this.getLoginEntryUrl(), {}, 'reLaunch');
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
logout() {
|
||||
uni.showModal({
|
||||
@@ -58,8 +16,8 @@ export default {
|
||||
content: this.$lang('logoutContent'),
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 统一的跳登录入口: 内部带防抖锁, 避免连发 reLaunch
|
||||
this.redirectToLoginOnce();
|
||||
this.clearLoginState();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -92,8 +50,8 @@ export default {
|
||||
this.$util.redirectTo('/pages_tool/member/cancellation');
|
||||
}
|
||||
} else if (res.code == -1) {
|
||||
// 统一的跳登录入口: 内部带防抖锁, 避免连发 reLaunch
|
||||
this.redirectToLoginOnce();
|
||||
this.clearLoginState();
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -111,15 +69,11 @@ export default {
|
||||
url: '/membercancel/api/membercancel/apply',
|
||||
success: rres => {
|
||||
if (rres.code >= 0) {
|
||||
// 注销申请提交成功后立刻清空登录态,
|
||||
// 避免用户通过标题栏/导航栏返回到会员中心等页面时仍能看到账号信息
|
||||
if (options.clearStateAndRedirect) {
|
||||
// 跳登录入口: 内部带防抖锁, 避免连发 reLaunch
|
||||
this.redirectToLoginOnce();
|
||||
} else {
|
||||
// 跳 cancelstatus (审核中), 也走 reLaunch 清栈
|
||||
this.clearLoginState();
|
||||
this.$util.redirectTo('/pages_tool/member/cancelstatus', {}, 'reLaunch');
|
||||
this.$util.redirectTo(this.$util.LOGIN_PAGE_URL);
|
||||
} else {
|
||||
this.$util.redirectTo('/pages_tool/member/cancelstatus');
|
||||
}
|
||||
} else {
|
||||
this.$util.showToast({
|
||||
|
||||
@@ -77,7 +77,7 @@ export default {
|
||||
setPublicShare() {
|
||||
let shareUrl = this.$config.h5Domain + '/pages_tool/notice/list';
|
||||
this.$util.setPublicShare({
|
||||
title: this.$lang('notice.list.title'),
|
||||
title: '公告',
|
||||
desc: '',
|
||||
link: shareUrl,
|
||||
imgUrl: this.siteInfo ? this.$util.img(this.siteInfo.logo_square) : ''
|
||||
@@ -85,7 +85,7 @@ export default {
|
||||
}
|
||||
},
|
||||
onShareAppMessage(res) {
|
||||
var title = this.$lang('notice.list.title');
|
||||
var title = '公告';
|
||||
var path = '/pages_tool/notice/list';
|
||||
return {
|
||||
title: title,
|
||||
|
||||
@@ -51,7 +51,7 @@ export default {
|
||||
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
|
||||
} else {
|
||||
this.$util.showToast({
|
||||
title: this.$lang('pay.index.payInfoNotFound')
|
||||
title: '未获取到支付信息!'
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.$util.redirectTo(this.$util.INDEX_PAGE_URL);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<view :style="themeColor" class="vr-page">
|
||||
<web-view :src="src" class="vr-webview" @titleUpdate="forceCustomTitle"></web-view>
|
||||
<view :style="themeColor">
|
||||
<web-view :src="src"></web-view>
|
||||
<!-- 顶部导航覆盖层:使用 cover-view 兼容微信小程序的 web-view 原生组件 -->
|
||||
<cover-view class="vr-nav-back icondiy icon-system-arrow-go-back-line" @click="navigateBack"></cover-view>
|
||||
<cover-view class="vr-nav-home icondiy icon-system-home-2-line" @click="goHome"></cover-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -8,73 +11,72 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
src: '',
|
||||
title: '',
|
||||
customTitle: '',
|
||||
titleTimer: null
|
||||
src: ''
|
||||
};
|
||||
},
|
||||
onLoad(option) {
|
||||
this.customTitle = option?.title || '工厂展示';
|
||||
this.title = this.customTitle;
|
||||
uni.setNavigationBarTitle({ title: this.customTitle });
|
||||
// this.src = decodeURIComponent(option.src);
|
||||
this.$api.sendRequest({
|
||||
url: '/api/config/defaultvr',
|
||||
success: res => {
|
||||
if (res.code == 0) {
|
||||
this.customTitle = res.data.title;
|
||||
this.title = this.customTitle;
|
||||
uni.setNavigationBarTitle({ title: this.customTitle });
|
||||
this.src = res.data.url;
|
||||
this.startTitleGuard();
|
||||
uni.setNavigationBarTitle({
|
||||
title:res.data.title
|
||||
})
|
||||
this.src = res.data.url
|
||||
}
|
||||
},
|
||||
fail: res => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onShow() {
|
||||
if (this.customTitle) {
|
||||
uni.setNavigationBarTitle({ title: this.customTitle });
|
||||
}
|
||||
},
|
||||
onUnload() {
|
||||
this.clearTitleGuard();
|
||||
},
|
||||
methods: {
|
||||
forceCustomTitle() {
|
||||
uni.setNavigationBarTitle({ title: this.customTitle });
|
||||
},
|
||||
startTitleGuard() {
|
||||
this.clearTitleGuard();
|
||||
let count = 0;
|
||||
this.titleTimer = setInterval(() => {
|
||||
uni.setNavigationBarTitle({ title: this.customTitle });
|
||||
count++;
|
||||
if (count >= 10) {
|
||||
this.clearTitleGuard();
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
clearTitleGuard() {
|
||||
if (this.titleTimer) {
|
||||
clearInterval(this.titleTimer);
|
||||
this.titleTimer = null;
|
||||
navigateBack() {
|
||||
const pages = getCurrentPages();
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
});
|
||||
} else {
|
||||
// 直接进入的场景(分享链接 / 外部链接 / 扫码等),页面栈只有当前页,
|
||||
// navigateBack 会失败。重定向到首页,保证用户能离开 VR 页面。
|
||||
uni.reLaunch({
|
||||
url: this.$util.PAGE_KEY_MAP.PAGE_INDEX
|
||||
});
|
||||
}
|
||||
},
|
||||
goHome() {
|
||||
uni.reLaunch({
|
||||
url: this.$util.PAGE_KEY_MAP.PAGE_INDEX
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.vr-page {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
.vr-nav-back,
|
||||
.vr-nav-home {
|
||||
position: absolute;
|
||||
top: 34rpx;
|
||||
z-index: 5;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.vr-webview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.vr-nav-back {
|
||||
left: 34rpx;
|
||||
line-height: 60rpx;
|
||||
}
|
||||
|
||||
.vr-nav-home {
|
||||
right: 34rpx;
|
||||
font-size: 40rpx;
|
||||
line-height: 72rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$target = 'C:\Program Files\Huawei QuickApp IDE\resources\app\extensions\deveco-debug\lib\toolkit\nodeTool'
|
||||
|
||||
$before = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||
|
||||
Write-Host '=== Before: target entries in Machine PATH ==='
|
||||
$before -split ';' | ForEach-Object {
|
||||
if ($_.Trim() -ieq $target) {
|
||||
Write-Host (" [" + $_ + "]")
|
||||
}
|
||||
}
|
||||
|
||||
$entries = $before -split ';'
|
||||
$kept = New-Object System.Collections.Generic.List[string]
|
||||
$removed = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($e in $entries) {
|
||||
$t = $e.Trim()
|
||||
if ($t -ieq $target) {
|
||||
$removed.Add($e)
|
||||
} elseif ($t -ne '') {
|
||||
$kept.Add($e)
|
||||
}
|
||||
}
|
||||
$after = [string]::Join(';', $kept.ToArray())
|
||||
|
||||
if ($removed.Count -eq 0) {
|
||||
Write-Host 'Target entry not found, nothing to do.'
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
[Environment]::SetEnvironmentVariable('Path', $after, 'Machine')
|
||||
Write-Host ''
|
||||
Write-Host '=== Result ==='
|
||||
Write-Host (" Removed entries: " + $removed.Count)
|
||||
foreach ($r in $removed) { Write-Host (" - " + $r) }
|
||||
Write-Host (" Remaining entries: " + $kept.Count)
|
||||
Write-Host ' Write OK.'
|
||||
} catch {
|
||||
Write-Host ''
|
||||
Write-Host (' Write FAILED (admin required?): ' + $_.Exception.Message)
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '=== Verify after write ==='
|
||||
$verify = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||
$stillThere = 0
|
||||
foreach ($e in ($verify -split ';')) {
|
||||
if ($e.Trim() -ieq $target) { $stillThere++ }
|
||||
}
|
||||
if ($stillThere -gt 0) {
|
||||
Write-Host ' FAIL: target still present.'
|
||||
exit 1
|
||||
} else {
|
||||
Write-Host ' OK: target no longer in Machine PATH.'
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Note: env var change does NOT affect already-running processes.'
|
||||
Write-Host 'Reopen cmd / Git Bash / HBuilderX to see updated where node.'
|
||||
@@ -1,209 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
/**
|
||||
* scripts/check-subpackage-sync.js
|
||||
*
|
||||
* 检查 mp-harmony 项目的子包 HAR 配置在 4 个地方是否同步:
|
||||
* 1. mp-harmony/build-profile.json5 的 modules 数组
|
||||
* 2. entry/oh-package.json5 的 dependencies
|
||||
* 3. entry/har-deps-required.json5 的 requiredHarDeps (单一事实源)
|
||||
* 4. ascf_build 实际目录
|
||||
*
|
||||
* 任何一处不一致 → 立即报错并指出哪一处需要修改。
|
||||
* 防止新增/删除子包时漏改某处导致 build/runtime 故障。
|
||||
*
|
||||
* 使用:
|
||||
* node scripts/check-subpackage-sync.js
|
||||
* node scripts/check-subpackage-sync.js --strict # 不一致即 exit 1 (CI 用)
|
||||
*
|
||||
* 输出:
|
||||
* ✓ 5 个子包配置完全一致
|
||||
* 或
|
||||
* ✘ 缺失 [pages_extra] 在以下位置:
|
||||
* - build-profile.json5 (mp-harmony)
|
||||
* - entry/oh-package.json5
|
||||
* 建议操作: ...
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const JSON5 = require('json5');
|
||||
|
||||
function loadJson5(filePath) {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
return JSON5.parse(raw);
|
||||
}
|
||||
|
||||
function exists(p) {
|
||||
try {
|
||||
fs.accessSync(p, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function listDirs(p) {
|
||||
try {
|
||||
return fs.readdirSync(p, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory())
|
||||
.map(e => e.name)
|
||||
.sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function diff(a, b) {
|
||||
const aSet = new Set(a);
|
||||
const bSet = new Set(b);
|
||||
const onlyA = [...aSet].filter(x => !bSet.has(x));
|
||||
const onlyB = [...bSet].filter(x => !aSet.has(x));
|
||||
return { onlyA, onlyB };
|
||||
}
|
||||
|
||||
function color(s, c) {
|
||||
if (!process.stdout.isTTY) return s;
|
||||
const codes = { red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', gray: '\x1b[90m', reset: '\x1b[0m' };
|
||||
return codes[c] + s + codes.reset;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const strict = process.argv.includes('--strict');
|
||||
|
||||
// === 单一事实源: har-deps-required.json5 ===
|
||||
const configPath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'entry', 'har-deps-required.json5');
|
||||
if (!exists(configPath)) {
|
||||
console.error(color(`✘ 缺少白名单配置: ${configPath}`, 'red'));
|
||||
process.exit(strict ? 1 : 0);
|
||||
}
|
||||
const config = loadJson5(configPath);
|
||||
const canonical = (config.requiredHarDeps || []).slice().sort();
|
||||
console.log(color('=== mp-harmony 子包配置一致性检查 ===', 'cyan'));
|
||||
console.log(`白名单 (har-deps-required.json5): ${canonical.join(', ')}\n`);
|
||||
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
// === 检查 1: mp-harmony/build-profile.json5 的 modules 数组 ===
|
||||
const buildProfilePath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'build-profile.json5');
|
||||
const buildProfileModules = [];
|
||||
if (!exists(buildProfilePath)) {
|
||||
errors.push(`build-profile.json5 不存在: ${buildProfilePath}`);
|
||||
} else {
|
||||
try {
|
||||
const bp = loadJson5(buildProfilePath);
|
||||
// modules 是顶层数组, 包含 entry + 5 个子包
|
||||
// entry 也算一个 module, 但它不在我们的白名单里
|
||||
const moduleNames = (bp.modules || []).map(m => m.name).filter(n => n && n !== 'entry');
|
||||
buildProfileModules.push(...moduleNames);
|
||||
const d = diff(canonical, buildProfileModules);
|
||||
if (d.onlyA.length) {
|
||||
errors.push(`build-profile.json5 缺失子包: [${d.onlyA.join(', ')}]`);
|
||||
console.log(` ${color('✘', 'red')} build-profile.json5: 缺失 [${d.onlyA.join(', ')}]`);
|
||||
} else if (d.onlyB.length) {
|
||||
errors.push(`build-profile.json5 多了子包: [${d.onlyB.join(', ')}]`);
|
||||
console.log(` ${color('✘', 'red')} build-profile.json5: 多了 [${d.onlyB.join(', ')}]`);
|
||||
} else {
|
||||
console.log(` ${color('✔', 'green')} build-profile.json5: ${buildProfileModules.length} 个子包模块与白名单一致`);
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push(`build-profile.json5 解析失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// === 检查 2: entry/oh-package.json5 的 dependencies ===
|
||||
const ohPkgPath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'entry', 'oh-package.json5');
|
||||
const ohPkgDeps = [];
|
||||
if (!exists(ohPkgPath)) {
|
||||
errors.push(`entry/oh-package.json5 不存在: ${ohPkgPath}`);
|
||||
} else {
|
||||
try {
|
||||
const op = loadJson5(ohPkgPath);
|
||||
const deps = op.dependencies || {};
|
||||
// 提取 5 个 file: 依赖 (key 是包名)
|
||||
ohPkgDeps.push(...Object.keys(deps).filter(k => deps[k] && deps[k].startsWith('file:')));
|
||||
const d = diff(canonical, ohPkgDeps);
|
||||
if (d.onlyA.length) {
|
||||
errors.push(`entry/oh-package.json5 缺失 file: 依赖: [${d.onlyA.join(', ')}]`);
|
||||
console.log(` ${color('✘', 'red')} entry/oh-package.json5: 缺失 [${d.onlyA.join(', ')}]`);
|
||||
} else if (d.onlyB.length) {
|
||||
warnings.push(`entry/oh-package.json5 多了 file: 依赖: [${d.onlyB.join(', ')}] (可能是新的子包还未更新白名单)`);
|
||||
console.log(` ${color('⚠', 'yellow')} entry/oh-package.json5: 多了 [${d.onlyB.join(', ')}]`);
|
||||
} else {
|
||||
console.log(` ${color('✔', 'green')} entry/oh-package.json5: ${ohPkgDeps.length} 个 file: 依赖与白名单一致`);
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push(`entry/oh-package.json5 解析失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// === 检查 3: ascf_build 实际目录 ===
|
||||
const ascfBuildRoot = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'ascf', 'ascf_build');
|
||||
const ascfBuildDirs = listDirs(ascfBuildRoot);
|
||||
const ascfBuildD = diff(canonical, ascfBuildDirs);
|
||||
if (ascfBuildD.onlyA.length) {
|
||||
errors.push(`ascf_build/ 目录缺失子包: [${ascfBuildD.onlyA.join(', ')}]`);
|
||||
console.log(` ${color('✘', 'red')} ascf_build/: 缺失 [${ascfBuildD.onlyA.join(', ')}]`);
|
||||
} else if (ascfBuildD.onlyB.length) {
|
||||
warnings.push(`ascf_build/ 多了子包: [${ascfBuildD.onlyB.join(', ')}] (可能是历史遗留)`);
|
||||
console.log(` ${color('⚠', 'yellow')} ascf_build/: 多了 [${ascfBuildD.onlyB.join(', ')}]`);
|
||||
} else {
|
||||
console.log(` ${color('✔', 'green')} ascf_build/: ${ascfBuildDirs.length} 个子包目录与白名单一致`);
|
||||
}
|
||||
|
||||
// === 检查 4: 每个子包的关键文件是否齐全 ===
|
||||
console.log(color('--- 子包关键文件检查 ---', 'cyan'));
|
||||
for (const pkg of canonical) {
|
||||
const pkgRoot = path.join(ascfBuildRoot, pkg);
|
||||
const requiredFiles = [
|
||||
'oh-package.json5',
|
||||
'build-profile.json5',
|
||||
'hvigorfile.ts',
|
||||
'obfuscation-rules.txt',
|
||||
'src/main/module.json5',
|
||||
];
|
||||
const missing = requiredFiles.filter(f => !exists(path.join(pkgRoot, f)));
|
||||
if (missing.length) {
|
||||
errors.push(`${pkg} 缺失文件: [${missing.join(', ')}]`);
|
||||
console.log(` ${color('✘', 'red')} ${pkg}: 缺失 [${missing.join(', ')}]`);
|
||||
} else {
|
||||
console.log(` ${color('✔', 'green')} ${pkg}: 关键文件齐备`);
|
||||
}
|
||||
}
|
||||
|
||||
// === 总结 ===
|
||||
console.log();
|
||||
if (warnings.length) {
|
||||
console.log(color(`警告 (${warnings.length}):`, 'yellow'));
|
||||
for (const w of warnings) console.log(' - ' + w);
|
||||
}
|
||||
if (errors.length) {
|
||||
console.log();
|
||||
console.log(color(`错误 (${errors.length}):`, 'red'));
|
||||
for (const e of errors) console.log(' - ' + e);
|
||||
console.log();
|
||||
console.log(color('✘ 子包配置不一致, 鸿蒙构建可能失败。', 'red'));
|
||||
console.log(color(' 修复步骤:', 'gray'));
|
||||
console.log(color(' 1. 修改 entry/har-deps-required.json5 的 requiredHarDeps (单一事实源)', 'gray'));
|
||||
console.log(color(' 2. 在 ascf_build/<pkg>/ 创建新子包模板 (含 oh-package.json5 / build-profile.json5 / ...) ', 'gray'));
|
||||
console.log(color(' 3. 在 mp-harmony/build-profile.json5 的 modules 数组加新模块', 'gray'));
|
||||
console.log(color(' 4. 在 entry/oh-package.json5 的 dependencies 加 "file:../ascf/ascf_build/<pkg>"', 'gray'));
|
||||
console.log(color(' 5. 在 DevEco Sync 项目 (让 ohpm 处理 file: 依赖)', 'gray'));
|
||||
console.log(color(' 6. 跑 node scripts/check-subpackage-sync.js --strict 验证', 'gray'));
|
||||
process.exit(strict ? 1 : 0);
|
||||
}
|
||||
|
||||
console.log(color(`✔ 5 个子包配置完全一致 (${canonical.length} 个包)`, 'green'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (e) {
|
||||
console.error(color(`脚本异常: ${e.stack || e.message}`, 'red'));
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// 检查 20 个硬编码导航标题的页面,其语言包是否都有 title key
|
||||
const paths = [
|
||||
['pages_goods/category','商品分类'],
|
||||
['pages_goods/cart','购物车'],
|
||||
['pages_goods/detail','商品详情'],
|
||||
['pages_goods/list','商品列表'],
|
||||
['pages_promotion/merch/merchcategory','入驻商户'],
|
||||
['pages_promotion/merch/detail','商户首页'],
|
||||
['pages_promotion/fenxiao/ranking_list','排行榜'],
|
||||
['pages_promotion/fenxiao/child_fenxiao','分销商'],
|
||||
['pages_tool/ai-chat/index','AI智能客服'],
|
||||
['pages_tool/contact/contact','电子名片'],
|
||||
['pages_tool/member/info','个人资料'],
|
||||
['pages_tool/order/activist','退款'],
|
||||
['pages_tool/login/login','登录'],
|
||||
['pages_tool/weapp/order_shipping','小程序发货'],
|
||||
['pages_tool/seal/structure','结构选型'],
|
||||
['pages_tool/seal/medium/search','介质材料搜索'],
|
||||
['pages_tool/seal/medium/detail','搜索结果'],
|
||||
['pages_tool/cases/index','案例展示'],
|
||||
['pages_tool/files/list','查看文件'],
|
||||
['pages_tool/file-preview/file-preview','文件预览'],
|
||||
];
|
||||
|
||||
function toLang(p) {
|
||||
const parts = p.split('/');
|
||||
if (parts[0] === 'pages_tool') return parts.slice(1).join('/');
|
||||
if (parts[0] === 'pages_goods') return 'goods/' + parts.slice(1).join('/');
|
||||
if (parts[0] === 'pages_member') return 'member/' + parts.slice(1).join('/');
|
||||
if (parts[0] === 'pages_order') return 'order/' + parts.slice(1).join('/');
|
||||
if (parts[0] === 'pages_promotion') {
|
||||
if (['point','fenxiao','merch'].includes(parts[1])) return parts.slice(1).join('/');
|
||||
return parts.slice(1).join('/');
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
for (const [p, hardcoded] of paths) {
|
||||
const lp = toLang(p);
|
||||
for (const loc of ['zh-cn','en-us']) {
|
||||
const fp = path.join('lang', loc, lp + '.js');
|
||||
if (!fs.existsSync(fp)) {
|
||||
console.log(`MISSING ${loc}/${lp}`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const m = require(path.resolve(fp)).lang;
|
||||
const title = m && m.title;
|
||||
const status = title === undefined ? 'NO title' : (title === hardcoded ? 'ZH-only?' : 'OK');
|
||||
console.log(`${loc}/${lp}.title = ${JSON.stringify(title)} [${status}]`);
|
||||
} catch (e) {
|
||||
console.log(`ERR ${loc}/${lp}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,429 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
/**
|
||||
* scripts/fix-subpackage-module-names.js
|
||||
*
|
||||
* 修正 ascf_build/<pkg>/ 子包 HAR 元数据中的 name 字段,确保鸿蒙工具链
|
||||
* (ohpm install / hvigor schema / 运行时依赖解析) 一致通过。
|
||||
*
|
||||
* 涉及两个文件:
|
||||
* 1. ascf_build/<pkg>/oh-package.json5 — top-level `name`
|
||||
* ohpm install 会读取这个字段,空字符串会报:
|
||||
* "00604007 Field IS Empty Error: The name of package .../pages_tool cannot be empty"
|
||||
*
|
||||
* 2. ascf_build/<pkg>/src/main/module.json5 — `module.name`
|
||||
* hvigor build 时 schema 校验要求匹配 ^[a-zA-Z][0-9a-zA-Z_.]*$,
|
||||
* 空字符串会报:
|
||||
* "Schema validate failed: must match pattern ^[a-zA-Z][0-9a-zA-Z_.]*$"
|
||||
*
|
||||
* 修复策略:
|
||||
* 遍历 SUBPACKAGE_NAMES 数组里的包, 用包目录名覆盖两处的 name,
|
||||
* 保留其他字段 (version / description / dependencies / deviceTypes 等)。
|
||||
* 如果文件不存在, 按合理默认值新建一份。
|
||||
*
|
||||
* 使用方式:
|
||||
* # 命令 1: 直接跑这个脚本
|
||||
* node scripts/fix-subpackage-module-names.js
|
||||
*
|
||||
* # 命令 2: 作为模块 require
|
||||
* node -e "require('./scripts/fix-subpackage-module-names').fixSubpackageModuleNames()"
|
||||
*
|
||||
* # 命令 3: 由 vue.config.js 顶层 IIFE 自动调用
|
||||
* # (HBuilderX / vue-cli-service / node -e 触发时, 自动执行)
|
||||
*
|
||||
* # CI 兜底校验:
|
||||
* node stub-app-projects/mp-harmony/entry/scripts/check-har-deps.js --strict
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SUBPACKAGE_NAMES = [
|
||||
'components_diy',
|
||||
'pages_goods',
|
||||
'pages_order',
|
||||
'pages_promotion',
|
||||
'pages_tool',
|
||||
];
|
||||
|
||||
/**
|
||||
* 修正 ascf_build/<pkg>/ 子包的 name 字段 (oh-package.json5 + module.json5)
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.ascfBuildRoot] - ascf_build 根目录, 默认基于本脚本位置推算
|
||||
* @param {boolean} [opts.silent=false] - true 时不打日志
|
||||
* @returns {{fixedCount: number, results: Array<{pkg: string, file: string, status: 'created'|'fixed'|'unchanged'|'error', message?: string}>}}
|
||||
*/
|
||||
function fixSubpackageModuleNames({ ascfBuildRoot, silent = false } = {}) {
|
||||
const root = ascfBuildRoot
|
||||
|| path.resolve(__dirname, '..', 'stub-app-projects', 'mp-harmony', 'ascf', 'ascf_build');
|
||||
const log = silent ? () => {} : console.log;
|
||||
log(`\n🔧 [fix-subpackage-module-names] 修正 ascf_build 子包 name (oh-package.json5 + module.json5)...`);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const pkg of SUBPACKAGE_NAMES) {
|
||||
results.push(...fixOhPackageJson5({ pkg, root, silent: true, log }));
|
||||
results.push(...fixModuleJson5({ pkg, root, silent: true, log }));
|
||||
}
|
||||
|
||||
const fixedCount = results.filter(r => r.status === 'fixed' || r.status === 'created').length;
|
||||
log(` 📊 修正完成: ${fixedCount} 个文件被更新\n`);
|
||||
return { fixedCount, results };
|
||||
}
|
||||
|
||||
/**
|
||||
* 修正 ascf_build/<pkg>/oh-package.json5 的 top-level `name` 字段
|
||||
*
|
||||
* 默认内容参考项目内已有 HAR (components_diy / pages_goods 等):
|
||||
* {
|
||||
* "name": "<pkg>",
|
||||
* "version": "1.0.0",
|
||||
* "description": "Please describe the basic information.",
|
||||
* "main": "Index.ets",
|
||||
* "author": "",
|
||||
* "license": "Apache-2.0",
|
||||
* "packageType": "InterfaceHar",
|
||||
* "dependencies": {}
|
||||
* }
|
||||
*/
|
||||
function fixOhPackageJson5({ pkg, root, silent, log }) {
|
||||
const ohPkgJson5Path = path.join(root, pkg, 'oh-package.json5');
|
||||
const defaultDoc = {
|
||||
name: pkg,
|
||||
version: '1.0.0',
|
||||
description: 'Please describe the basic information.',
|
||||
main: 'Index.ets',
|
||||
author: '',
|
||||
license: 'Apache-2.0',
|
||||
packageType: 'InterfaceHar',
|
||||
dependencies: {},
|
||||
};
|
||||
|
||||
let current = null;
|
||||
if (fs.existsSync(ohPkgJson5Path)) {
|
||||
try {
|
||||
// oh-package.json5 是 JSON5, 可能含 // 注释。Node 内置 JSON.parse 不能处理,
|
||||
// 用 stripJson5Comments + JSON.parse 处理最常见情况。
|
||||
const raw = fs.readFileSync(ohPkgJson5Path, 'utf8');
|
||||
const cleaned = stripJson5Comments(raw);
|
||||
current = JSON.parse(cleaned);
|
||||
} catch (e) {
|
||||
if (!silent) log(` ⚠️ ${pkg}/oh-package.json5 解析失败, 使用默认: ${e.message}`);
|
||||
current = null;
|
||||
}
|
||||
}
|
||||
|
||||
const relPath = `${pkg}/oh-package.json5`;
|
||||
if (!current || typeof current !== 'object') {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(ohPkgJson5Path), { recursive: true });
|
||||
fs.writeFileSync(ohPkgJson5Path, JSON.stringify(defaultDoc, null, 2) + '\n', 'utf8');
|
||||
if (!silent) log(` 🆕 ${relPath}: 已创建 (name="${pkg}")`);
|
||||
return [{ pkg, file: 'oh-package.json5', status: 'created' }];
|
||||
} catch (e) {
|
||||
if (!silent) log(` ❌ ${relPath}: 创建失败: ${e.message}`);
|
||||
return [{ pkg, file: 'oh-package.json5', status: 'error', message: e.message }];
|
||||
}
|
||||
}
|
||||
|
||||
const oldName = current.name;
|
||||
if (oldName !== pkg) {
|
||||
current.name = pkg;
|
||||
try {
|
||||
fs.writeFileSync(ohPkgJson5Path, JSON.stringify(current, null, 2) + '\n', 'utf8');
|
||||
if (!silent) log(` ✅ ${relPath}: name "${oldName || '(空)'}" → "${pkg}"`);
|
||||
return [{ pkg, file: 'oh-package.json5', status: 'fixed', oldName }];
|
||||
} catch (e) {
|
||||
if (!silent) log(` ❌ ${relPath}: 写入失败: ${e.message}`);
|
||||
return [{ pkg, file: 'oh-package.json5', status: 'error', message: e.message }];
|
||||
}
|
||||
}
|
||||
|
||||
if (!silent) log(` ✔️ ${relPath}: name = "${pkg}" (无须修正)`);
|
||||
return [{ pkg, file: 'oh-package.json5', status: 'unchanged' }];
|
||||
}
|
||||
|
||||
/**
|
||||
* 修正 ascf_build/<pkg>/src/main/module.json5 的 `module.name` 字段
|
||||
*/
|
||||
function fixModuleJson5({ pkg, root, silent, log }) {
|
||||
const moduleJson5Path = path.join(root, pkg, 'src', 'main', 'module.json5');
|
||||
const defaultDoc = {
|
||||
module: {
|
||||
name: pkg,
|
||||
type: 'shared',
|
||||
description: 'subpackage',
|
||||
deviceTypes: ['phone', 'tablet'],
|
||||
deliveryWithInstall: true,
|
||||
installationFree: true,
|
||||
},
|
||||
};
|
||||
|
||||
let current = null;
|
||||
if (fs.existsSync(moduleJson5Path)) {
|
||||
try {
|
||||
const raw = fs.readFileSync(moduleJson5Path, 'utf8');
|
||||
const cleaned = stripJson5Comments(raw);
|
||||
current = JSON.parse(cleaned);
|
||||
} catch (e) {
|
||||
if (!silent) log(` ⚠️ ${pkg}/src/main/module.json5 解析失败, 使用默认: ${e.message}`);
|
||||
current = null;
|
||||
}
|
||||
}
|
||||
|
||||
const relPath = `${pkg}/src/main/module.json5`;
|
||||
if (!current || !current.module || typeof current.module !== 'object') {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(moduleJson5Path), { recursive: true });
|
||||
fs.writeFileSync(moduleJson5Path, JSON.stringify(defaultDoc, null, 2) + '\n', 'utf8');
|
||||
if (!silent) log(` 🆕 ${relPath}: 已创建 (name="${pkg}")`);
|
||||
return [{ pkg, file: 'module.json5', status: 'created' }];
|
||||
} catch (e) {
|
||||
if (!silent) log(` ❌ ${relPath}: 创建失败: ${e.message}`);
|
||||
return [{ pkg, file: 'module.json5', status: 'error', message: e.message }];
|
||||
}
|
||||
}
|
||||
|
||||
const oldName = current.module.name;
|
||||
if (oldName !== pkg) {
|
||||
current.module.name = pkg;
|
||||
try {
|
||||
fs.writeFileSync(moduleJson5Path, JSON.stringify(current, null, 2) + '\n', 'utf8');
|
||||
if (!silent) log(` ✅ ${relPath}: module.name "${oldName || '(空)'}" → "${pkg}"`);
|
||||
return [{ pkg, file: 'module.json5', status: 'fixed', oldName }];
|
||||
} catch (e) {
|
||||
if (!silent) log(` ❌ ${relPath}: 写入失败: ${e.message}`);
|
||||
return [{ pkg, file: 'module.json5', status: 'error', message: e.message }];
|
||||
}
|
||||
}
|
||||
|
||||
if (!silent) log(` ✔️ ${relPath}: module.name = "${pkg}" (无须修正)`);
|
||||
return [{ pkg, file: 'module.json5', status: 'unchanged' }];
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单 JSON5 注释剥离
|
||||
* 处理 // 单行注释 和 /* 块注释 *\/, 跳过字符串字面量内的 //
|
||||
* 注意: 不处理裸 key、单引号字符串、Unicode 转义等 JSON5 扩展
|
||||
* (本项目实际遇到的文件都是标准 JSON + 注释, 足够覆盖)
|
||||
*/
|
||||
function stripJson5Comments(input) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
const n = input.length;
|
||||
while (i < n) {
|
||||
const c = input[i];
|
||||
const next = i + 1 < n ? input[i + 1] : '';
|
||||
if (c === '/' && next === '*') {
|
||||
i += 2;
|
||||
while (i < n && !(input[i] === '*' && input[i + 1] === '/')) i++;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c === '/' && next === '/') {
|
||||
i += 2;
|
||||
while (i < n && input[i] !== '\n') i++;
|
||||
continue;
|
||||
}
|
||||
if (c === '"' || c === "'") {
|
||||
const quote = c;
|
||||
out += c;
|
||||
i++;
|
||||
while (i < n) {
|
||||
const sc = input[i];
|
||||
if (sc === '\\') {
|
||||
out += sc + (input[i + 1] || '');
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
out += sc;
|
||||
i++;
|
||||
if (sc === quote) break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out += c;
|
||||
i++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 顶层 IIFE: 直接执行脚本时自动跑 (CLI 行为)
|
||||
function main() {
|
||||
try {
|
||||
fixSubpackageModuleNames();
|
||||
} catch (e) {
|
||||
console.error('[fix-subpackage-module-names] 执行失败:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步子包配置一致性检查 (被 vue.config.js 顶层 IIFE 调用)
|
||||
*
|
||||
* 用途: HBuilderX 触发 require('./vue.config.js') 时, 在修完 name 之后
|
||||
* 跑一次 4 处一致性检查, 立即告诉用户哪些地方没改。
|
||||
*
|
||||
* 检查项 (复用 SUBPACKAGE_NAMES 数组):
|
||||
* 1. mp-harmony/build-profile.json5 的 modules 数组 (排除 entry)
|
||||
* 2. entry/oh-package.json5 的 file: 依赖
|
||||
* 3. ascf_build 实际目录
|
||||
*
|
||||
* 与 scripts/check-subpackage-sync.js 的区别:
|
||||
* - check-subpackage-sync.js: CLI/CI 用, --strict 可 exit 1
|
||||
* - runSyncCheck: vue.config.js IIFE 用, 默认不抛错, 仅打印
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {boolean} [opts.silent=false] - true 时不打日志
|
||||
* @param {boolean} [opts.exitOnError=false] - true 时不一致即抛错 (CLI 模式)
|
||||
* @returns {{ok: boolean, errors: string[], warnings: string[]}}
|
||||
*/
|
||||
function runSyncCheck({ silent = false, exitOnError = false } = {}) {
|
||||
const log = silent ? () => {} : console.log;
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
// 读取白名单 (从 har-deps-required.json5 拿, 与 check-subpackage-sync.js 保持一致)
|
||||
// 注: 优先用 json5 npm 包 (项目已装), 失败则回退到内置 SUBPACKAGE_NAMES
|
||||
const configPath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'entry', 'har-deps-required.json5');
|
||||
let canonical = SUBPACKAGE_NAMES.slice().sort();
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const raw = fs.readFileSync(configPath, 'utf8');
|
||||
let cfg = null;
|
||||
try {
|
||||
// 优先: json5 包 (支持裸 key, 注释, 单引号字符串)
|
||||
cfg = require('json5').parse(raw);
|
||||
} catch (e1) {
|
||||
// 回退: 手写 JSON5 解析 (剥注释后用 JSON.parse)
|
||||
try {
|
||||
const cleaned = stripJson5Comments(raw);
|
||||
cfg = JSON.parse(cleaned);
|
||||
} catch (e2) {
|
||||
if (!silent) console.warn(` ⚠️ har-deps-required.json5 解析失败 (json5: ${e1.message}; json: ${e2.message}), 用内置 SUBPACKAGE_NAMES`);
|
||||
}
|
||||
}
|
||||
if (cfg && Array.isArray(cfg.requiredHarDeps) && cfg.requiredHarDeps.length) {
|
||||
canonical = cfg.requiredHarDeps.slice().sort();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (!silent) console.warn(' ⚠️ 读取 har-deps-required.json5 失败, 用内置 SUBPACKAGE_NAMES:', e.message);
|
||||
}
|
||||
|
||||
const canonicalSet = new Set(canonical);
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
// 工具: 列出某目录下的子目录
|
||||
function listSubdirs(p) {
|
||||
try {
|
||||
return fs.readdirSync(p, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory())
|
||||
.map(e => e.name)
|
||||
.sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function diff(b) {
|
||||
const bSet = new Set(b);
|
||||
return {
|
||||
onlyA: [...canonicalSet].filter(x => !bSet.has(x)),
|
||||
onlyB: [...bSet].filter(x => !canonicalSet.has(x)),
|
||||
};
|
||||
}
|
||||
|
||||
// === 检查 1: build-profile.json5 ===
|
||||
const buildProfilePath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'build-profile.json5');
|
||||
let buildProfileModules = [];
|
||||
if (!fs.existsSync(buildProfilePath)) {
|
||||
warnings.push(`build-profile.json5 不存在: ${buildProfilePath}`);
|
||||
} else {
|
||||
try {
|
||||
const raw = fs.readFileSync(buildProfilePath, 'utf8').replace(/\/\/.*$/gm, '');
|
||||
const bp = JSON.parse(raw);
|
||||
buildProfileModules = (bp.modules || []).map(m => m.name).filter(n => n && n !== 'entry');
|
||||
} catch (e) {
|
||||
errors.push(`build-profile.json5 解析失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
const d1 = diff(buildProfileModules);
|
||||
if (d1.onlyA.length) errors.push(`build-profile.json5 缺失子包: [${d1.onlyA.join(', ')}]`);
|
||||
if (d1.onlyB.length) errors.push(`build-profile.json5 多了子包: [${d1.onlyB.join(', ')}]`);
|
||||
|
||||
// === 检查 2: entry/oh-package.json5 ===
|
||||
const ohPkgPath = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'entry', 'oh-package.json5');
|
||||
let ohPkgDeps = [];
|
||||
if (!fs.existsSync(ohPkgPath)) {
|
||||
warnings.push(`entry/oh-package.json5 不存在: ${ohPkgPath}`);
|
||||
} else {
|
||||
try {
|
||||
const raw = fs.readFileSync(ohPkgPath, 'utf8').replace(/\/\/.*$/gm, '');
|
||||
const op = JSON.parse(raw);
|
||||
const deps = op.dependencies || {};
|
||||
ohPkgDeps = Object.keys(deps).filter(k => deps[k] && deps[k].startsWith('file:'));
|
||||
} catch (e) {
|
||||
errors.push(`entry/oh-package.json5 解析失败: ${e.message}`);
|
||||
}
|
||||
}
|
||||
const d2 = diff(ohPkgDeps);
|
||||
if (d2.onlyA.length) errors.push(`entry/oh-package.json5 缺失 file: 依赖: [${d2.onlyA.join(', ')}]`);
|
||||
if (d2.onlyB.length) warnings.push(`entry/oh-package.json5 多了 file: 依赖: [${d2.onlyB.join(', ')}] (可能是新子包未更新白名单)`);
|
||||
|
||||
// === 检查 3: ascf_build 实际目录 ===
|
||||
const ascfBuildRoot = path.join(ROOT, 'stub-app-projects', 'mp-harmony', 'ascf', 'ascf_build');
|
||||
const ascfBuildDirs = listSubdirs(ascfBuildRoot);
|
||||
const d3 = diff(ascfBuildDirs);
|
||||
if (d3.onlyA.length) errors.push(`ascf_build/ 目录缺失子包: [${d3.onlyA.join(', ')}]`);
|
||||
if (d3.onlyB.length) warnings.push(`ascf_build/ 多了子包: [${d3.onlyB.join(', ')}] (可能是历史遗留)`);
|
||||
|
||||
// === 输出 ===
|
||||
if (silent) {
|
||||
// 静默模式仅返回结果
|
||||
return { ok: errors.length === 0, errors, warnings };
|
||||
}
|
||||
|
||||
const c = (s, color) => {
|
||||
if (!process.stdout.isTTY) return s;
|
||||
const codes = { red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', gray: '\x1b[90m', reset: '\x1b[0m' };
|
||||
return codes[color] + s + codes.reset;
|
||||
};
|
||||
|
||||
if (errors.length === 0 && warnings.length === 0) {
|
||||
log(c(`\n🔍 [sync check] ✓ ${canonical.length} 个子包在 3 处配置完全一致 (build-profile / oh-package / ascf_build)`, 'green'));
|
||||
} else {
|
||||
log(c(`\n🔍 [sync check] ${errors.length === 0 ? '⚠️' : '✘'} 4 处子包配置不一致:`, errors.length === 0 ? 'yellow' : 'red'));
|
||||
for (const w of warnings) log(c(` ⚠️ ${w}`, 'yellow'));
|
||||
for (const e of errors) log(c(` ✘ ${e}`, 'red'));
|
||||
if (errors.length > 0) {
|
||||
log(c(`\n 修复: 同步更新以下 4 个文件 (whitelist 是 har-deps-required.json5):`, 'gray'));
|
||||
log(c(` 1. entry/har-deps-required.json5 (whitelist, 单一事实源)`, 'gray'));
|
||||
log(c(` 2. mp-harmony/build-profile.json5 modules 数组`, 'gray'));
|
||||
log(c(` 3. entry/oh-package.json5 dependencies 加 "file:../ascf/ascf_build/<pkg>"`, 'gray'));
|
||||
log(c(` 4. ascf_build/<pkg>/ 实际目录`, 'gray'));
|
||||
log(c(` DevEco Build 仍会因 ohpm 校验而失败, 修完再发`, 'gray'));
|
||||
}
|
||||
}
|
||||
log('');
|
||||
|
||||
if (exitOnError && errors.length > 0) {
|
||||
throw new Error(`[sync check] ${errors.length} 个不一致`);
|
||||
}
|
||||
|
||||
return { ok: errors.length === 0, errors, warnings };
|
||||
}
|
||||
|
||||
// 判断是否作为 CLI 直接执行 (vs 作为模块被 require)
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fixSubpackageModuleNames,
|
||||
runSyncCheck,
|
||||
SUBPACKAGE_NAMES,
|
||||
};
|
||||
@@ -1,264 +0,0 @@
|
||||
{
|
||||
"uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue": [
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "取消",
|
||||
"line": 6
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "确定",
|
||||
"line": 7
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "{{ (nowDate.year || '') + '年' + (nowDate.month || '') + '月' }}",
|
||||
"line": 15
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "日",
|
||||
"line": 27
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "一",
|
||||
"line": 28
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "二",
|
||||
"line": 29
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "三",
|
||||
"line": 30
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "四",
|
||||
"line": 31
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "五",
|
||||
"line": 32
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "六",
|
||||
"line": 33
|
||||
}
|
||||
],
|
||||
"pages_promotion/fenxiao/withdraw_list.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "已转账",
|
||||
"line": 45
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "待审核",
|
||||
"line": 49
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "已审核",
|
||||
"line": 53
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "已拒绝",
|
||||
"line": 57
|
||||
}
|
||||
],
|
||||
"uni_modules/mp-html/components/mp-html/node/node.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showActionSheet({\n itemList: ['保存图片'],\n success: () => {\n const save = path => {\n uni.saveImageToPhotosAlbum({\n ",
|
||||
"line": 289
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showToast({\n title: '链接已复制'\n })",
|
||||
"line": 368
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "保存成功",
|
||||
"line": 297
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "链接已复制",
|
||||
"line": 369
|
||||
}
|
||||
],
|
||||
"components-diy/diy-form.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showToast({\n\t\t\t\t\t\t\ttitle: '提交成功',\n\t\t\t\t\t\t\ticon: 'success'\n\t\t\t\t\t\t})",
|
||||
"line": 305
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showToast({\n\t\t\t\t\t\t\ttitle: res.message || '提交失败',\n\t\t\t\t\t\t\ticon: 'none'\n\t\t\t\t\t\t})",
|
||||
"line": 314
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "提交成功",
|
||||
"line": 306
|
||||
}
|
||||
],
|
||||
"uni_modules/uni-count-down/components/uni-count-down/uni-count-down.vue": [
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "view",
|
||||
"text": "{{ showColon ? ':' : '时' }}",
|
||||
"line": 19
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "view",
|
||||
"text": "{{ showColon ? ':' : '分' }}",
|
||||
"line": 27
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "view",
|
||||
"text": "秒",
|
||||
"line": 35
|
||||
}
|
||||
],
|
||||
"components/ns-goods-recommend/ns-goods-recommend.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "猜你喜欢",
|
||||
"line": 13
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "购买",
|
||||
"line": 20
|
||||
}
|
||||
],
|
||||
"components-diy/diy-channel-list.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showToast({\n\t\t\t\t\t\t\t\ttitle: '视频号仅支持在微信小程序环境中播放',\n\t\t\t\t\t\t\t\ticon: 'none',\n\t\t\t\t\t\t\t\tduration: 2000\n\t\t\t\t\t\t\t})",
|
||||
"line": 207
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "视频号仅支持在微信小程序环境中播放",
|
||||
"line": 208
|
||||
}
|
||||
],
|
||||
"pages/index/public/js/index.js": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showModal({\n\t\t\t\t\ttitle: '版本信息',\n\t\t\t\t\tcontent: `提交 ID: ${v.commitId}\\n构建时间: ${v.buildTime}\\n${Config.baseUrl}`,\n\t\t\t\t\tshowCancel: false\n\t\t\t\t})",
|
||||
"line": 160
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "版本信息",
|
||||
"line": 161
|
||||
}
|
||||
],
|
||||
"uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue": [
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "{{ (nowDate.year||'') + ' 年 ' + ( nowDate.month||'') +' 月'}}",
|
||||
"line": 13
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "view",
|
||||
"text": "确认",
|
||||
"line": 87
|
||||
}
|
||||
],
|
||||
"uni_modules/uni-number-box/components/uni-number-box/uni-number-box.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "商品库存不足",
|
||||
"line": 131
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "商品最少购买",
|
||||
"line": 135
|
||||
}
|
||||
],
|
||||
"common/js/mp-share-default.js": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "小程序分享",
|
||||
"line": 85
|
||||
}
|
||||
],
|
||||
"common/js/splash-state.js": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showLoading({ title: opts.title || '加载中...', mask: false })",
|
||||
"line": 111
|
||||
}
|
||||
],
|
||||
"pages/index/index.vue": [
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "{{ diyLoadErrorMsg || '网络异常,请稍后重试' }}",
|
||||
"line": 8
|
||||
}
|
||||
],
|
||||
"pages_goods/public/js/list.js": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "购买",
|
||||
"line": 40
|
||||
}
|
||||
],
|
||||
"pages_tool/help/list.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "帮助中心",
|
||||
"line": 82
|
||||
}
|
||||
],
|
||||
"pages_tool/notice/list.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "公告",
|
||||
"line": 80
|
||||
}
|
||||
],
|
||||
"pages_tool/pay/index.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "未获取到支付信息!",
|
||||
"line": 54
|
||||
}
|
||||
],
|
||||
"uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "日期和时间",
|
||||
"line": 112
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,347 +35,6 @@ async function syncSiteToStub(mode = 'production') {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步 manifest.json 的 mp-harmony.privacy 配置到 stub 项目的 app.json
|
||||
*
|
||||
* 背景:
|
||||
* 鸿蒙元服务上架审核需要从 app.json 读 privacy.privacyContractUrl 等字段,
|
||||
* 实际生效的是编译产物里 ascf/ascf_src/app.json (ascf_build 拷贝自此)。
|
||||
* 但当前 build 流程不会自动从 manifest.json 注入 privacy 字段,
|
||||
* 如果只改 manifest.json 而忘记改 app.json, 审核时拿到的是旧 URL。
|
||||
*
|
||||
* 同步策略:
|
||||
* 1. 读取 manifest.json 的 mp-harmony.privacy, 期望里面含占位符 YOUR_DOMAIN / YOUR_UNIACID
|
||||
* 2. 从 site.js 解析 baseUrl (域名) + uniacid
|
||||
* 3. 把占位符替换成 site.js 里的真实值, 写入 stub app.json
|
||||
* 4. 若 manifest.json 中无 mp-harmony.privacy → 保留 app.json 现有配置 (向后兼容)
|
||||
* 5. 若 site.js 缺 baseUrl 或 uniacid → 跳过同步, 警告用户
|
||||
*
|
||||
* 设计取舍:
|
||||
* - manifest.json 用占位符占位 (避免每个 uniacid 切换都要改一次 manifest)
|
||||
* - site.js 永远是真实值来源 (这是项目里已经存在的部署配置)
|
||||
* - 同步在构建时执行, 一份配置适配所有 uniacid
|
||||
*
|
||||
* 依赖: syncSiteToStub 必须先于本函数执行 (在 main() 中已保证)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 鲁棒的 JSONC 注释剥离 (跳过字符串字面量, 只剥真正的注释)
|
||||
* 原因: manifest.json 顶部有 /* ... *\/ 注释, 用正则 `/\*[\s\S]*?\*\//g` 简单剥离时,
|
||||
* 会跨过字符串字面量, 把"https://..."注释中的字符拼到真实 URL 后面,
|
||||
* 引入换行符导致 JSON.parse 失败 (Bad control character)。
|
||||
* 必须逐字符扫描, 跳过字符串内的 /*、//。
|
||||
*/
|
||||
function stripJsonComments(input) {
|
||||
let out = '';
|
||||
let i = 0;
|
||||
const n = input.length;
|
||||
while (i < n) {
|
||||
const c = input[i];
|
||||
const next = i + 1 < n ? input[i + 1] : '';
|
||||
if (c === '/' && next === '*') {
|
||||
i += 2;
|
||||
while (i < n && !(input[i] === '*' && input[i + 1] === '/')) i++;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c === '/' && next === '/') {
|
||||
i += 2;
|
||||
while (i < n && input[i] !== '\n') i++;
|
||||
continue;
|
||||
}
|
||||
if (c === '"') {
|
||||
out += c;
|
||||
i++;
|
||||
while (i < n) {
|
||||
const sc = input[i];
|
||||
if (sc === '\\') {
|
||||
out += sc + (input[i + 1] || '');
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
out += sc;
|
||||
i++;
|
||||
if (sc === '"') break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out += c;
|
||||
i++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 site.js 提取 baseUrl + uniacid
|
||||
*
|
||||
* site.js 形如:
|
||||
* module.exports = {
|
||||
* baseUrl: "https://xcx.aigc-quickapp.com/",//修改域名
|
||||
* uniacid: 2482,//后台对应uniacid
|
||||
* };
|
||||
*
|
||||
* 不直接 require, 因为:
|
||||
* 1. site.js 在前端构建产物中用, 可能依赖一些运行时变量 (uni/Config/...)
|
||||
* 2. 同步逻辑要在 vue.config.js 触发, 不希望引入额外依赖
|
||||
* 3. regex 提取足够安全, 避免执行任意 JS
|
||||
*
|
||||
* @returns {{baseUrl: string, uniacid: string}|null} 解析失败返回 null
|
||||
*/
|
||||
function parseSiteConfig(siteJsSource) {
|
||||
// baseUrl: 单/双引号字符串, 可带尾斜杠
|
||||
const baseUrlMatch = siteJsSource.match(/baseUrl\s*:\s*['"]([^'"]+)['"]/)
|
||||
// uniacid: 数字或字符串
|
||||
const uniacidMatch = siteJsSource.match(/uniacid\s*:\s*['"]?(\d+)['"]?/)
|
||||
if (!baseUrlMatch || !uniacidMatch) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
baseUrl: baseUrlMatch[1],
|
||||
uniacid: uniacidMatch[1],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 baseUrl 拆成 protocol + host (无尾斜杠)
|
||||
* 因为 site.js 的 baseUrl 通常带尾斜杠, 占位符 YOUR_DOMAIN
|
||||
* 在 URL 中间出现 (例: https://YOUR_DOMAIN/api/xxx), 直接拼接
|
||||
* 会产生双斜杠. 这里规范化为"协议://域名(无尾斜杠)".
|
||||
*
|
||||
* @param {string} baseUrl 例: "https://xcx.aigc-quickapp.com/"
|
||||
* @returns {string} 例: "https://xcx.aigc-quickapp.com"
|
||||
*/
|
||||
function normalizeBaseUrlForPlaceholder(baseUrl) {
|
||||
// 去掉尾斜杠
|
||||
let url = baseUrl.replace(/\/+$/, '')
|
||||
// 若没有协议, 加 https:// (避免出现 //xcx... 这种相对协议)
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
url = 'https://' + url
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归替换隐私配置对象中的占位符
|
||||
* @param {*} value 任意 JSON 值
|
||||
* @param {object} siteConfig {baseUrl, uniacid}
|
||||
* @returns {*} 替换后的新值 (不修改原对象)
|
||||
*/
|
||||
function replacePlaceholders(value, siteConfig) {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
.replace(/YOUR_DOMAIN/g, siteConfig.baseUrl)
|
||||
.replace(/YOUR_UNIACID/g, siteConfig.uniacid)
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(v => replacePlaceholders(v, siteConfig))
|
||||
}
|
||||
if (value !== null && typeof value === 'object') {
|
||||
const out = {}
|
||||
for (const k of Object.keys(value)) {
|
||||
out[k] = replacePlaceholders(value[k], siteConfig)
|
||||
}
|
||||
return out
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义错误: 验证 app.json 写后内容失败时抛出
|
||||
* 上层 try/catch 会捕获并 process.exitCode = 1, 让 vue.config.js / npm run build 失败
|
||||
*/
|
||||
class AppJsonVerificationError extends Error {
|
||||
constructor(messages) {
|
||||
super('app.json 写后验证失败:\n - ' + messages.join('\n - '))
|
||||
this.name = 'AppJsonVerificationError'
|
||||
this.messages = messages
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证写完的 app.json 内容正确
|
||||
*
|
||||
* 关键字段:
|
||||
* - 顶级 privacy 块存在
|
||||
* - privacyContractUrl / serviceContractUrl 都以 https:// + baseUrl 开头
|
||||
* - URL 不含占位符 YOUR_DOMAIN / YOUR_UNIACID
|
||||
* - URL 不含双协议头 (https://https://)
|
||||
* - URL 不含双斜杠 (https://x.com//api)
|
||||
* - URL 的 uniacid= 参数值 == siteConfig.uniacid
|
||||
* - JSON 仍能正确 parse (没有序列化失败)
|
||||
*
|
||||
* 失败行为: 抛 AppJsonVerificationError, 包含所有不通过项
|
||||
*
|
||||
* @param {string} appJsonPath
|
||||
* @param {{baseUrl: string, uniacid: string}} siteConfig
|
||||
*/
|
||||
async function verifyWrittenAppJson(appJsonPath, siteConfig) {
|
||||
const errors = []
|
||||
|
||||
// 1. 重新读取 + parse
|
||||
let written
|
||||
try {
|
||||
const raw = await fsp.readFile(appJsonPath, 'utf8')
|
||||
written = JSON.parse(raw)
|
||||
} catch (e) {
|
||||
throw new AppJsonVerificationError([`文件读取/解析失败: ${e.message}`])
|
||||
}
|
||||
|
||||
// 2. 顶级 privacy 块
|
||||
const privacy = written.privacy
|
||||
if (!privacy || typeof privacy !== 'object') {
|
||||
throw new AppJsonVerificationError(['privacy 块缺失或不是对象'])
|
||||
}
|
||||
|
||||
// 3. URL 字段检查
|
||||
const urlFields = ['privacyContractUrl', 'serviceContractUrl']
|
||||
for (const field of urlFields) {
|
||||
const url = privacy[field]
|
||||
if (typeof url !== 'string') {
|
||||
errors.push(`${field} 缺失或不是字符串 (实际: ${typeof url})`)
|
||||
continue
|
||||
}
|
||||
|
||||
// 不含占位符
|
||||
if (/YOUR_DOMAIN|YOUR_UNIACID/.test(url)) {
|
||||
errors.push(`${field} 仍含占位符: ${url}`)
|
||||
}
|
||||
|
||||
// 不含双协议头
|
||||
if (/^https:\/\/https:|^http:\/\/http:/i.test(url)) {
|
||||
errors.push(`${field} 含双协议头: ${url}`)
|
||||
}
|
||||
|
||||
// 不含双斜杠 (协议头之后, 路径开始之前)
|
||||
// 关键: 截到第一个 ? 或 # 之前, 检查是否含 // (避免误报 query 里的 //)
|
||||
const afterProtocol = url.replace(/^https?:\/\//i, '')
|
||||
const queryStart = afterProtocol.search(/[?#]/)
|
||||
const beforeQuery = queryStart === -1 ? afterProtocol : afterProtocol.slice(0, queryStart)
|
||||
if (beforeQuery.includes('//')) {
|
||||
errors.push(`${field} 路径部分含双斜杠 (双斜杠 bug): ${url}`)
|
||||
}
|
||||
|
||||
// 必须以 https:// + siteConfig.baseUrl 开头
|
||||
const expectedPrefix = siteConfig.baseUrl + '/'
|
||||
if (!url.startsWith(expectedPrefix)) {
|
||||
errors.push(`${field} 不是以 "${expectedPrefix}" 开头 (实际: ${url.slice(0, expectedPrefix.length + 10)})`)
|
||||
}
|
||||
|
||||
// uniacid= 参数值 == siteConfig.uniacid
|
||||
const uniacidMatch = url.match(/[?&]uniacid=([^&]+)/)
|
||||
if (!uniacidMatch) {
|
||||
errors.push(`${field} 缺少 uniacid= 参数: ${url}`)
|
||||
} else if (uniacidMatch[1] !== siteConfig.uniacid) {
|
||||
errors.push(`${field} 的 uniacid=${uniacidMatch[1]} 不等于 site.js 的 uniacid=${siteConfig.uniacid}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. contractName 字段未被破坏 (manifest 里的 zh/en name 应该原样保留)
|
||||
const nameFields = ['privacyContractName', 'serviceContractName']
|
||||
for (const field of nameFields) {
|
||||
const name = privacy[field]
|
||||
if (!name || typeof name !== 'object' || !name.zh || !name.en) {
|
||||
errors.push(`${field} 结构损坏 (应有 zh/en): ${JSON.stringify(name)}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new AppJsonVerificationError(errors)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步 privacy 到**任意**目标 app.json 路径
|
||||
*
|
||||
* @param {string} appJsonPath 目标 app.json 文件绝对路径
|
||||
* @param {string} [label] 标识 (用于日志, 例 "stub" / "build")
|
||||
* @returns {Promise<{written: boolean, siteConfig?: object}>}
|
||||
*
|
||||
* 应用场景:
|
||||
* - syncPrivacyToPath('stub-app-projects/.../app.json', 'stub')
|
||||
* → 写 stub ascf_src/app.json (HBuilderX 之外的源)
|
||||
* - syncPrivacyToPath('unpackage/dist/build/.mp-harmony/app.json', 'build')
|
||||
* → 写 HBuilderX 构建产物的 app.json, **必须**在打 ZIP 之前
|
||||
* → 否则发行 ZIP 里的 app.json 仍是占位符
|
||||
*/
|
||||
async function syncPrivacyToPath(appJsonPath, label = '') {
|
||||
try {
|
||||
const cwd = path.join(__dirname, '..');
|
||||
const manifestPath = path.join(cwd, 'manifest.json');
|
||||
const siteJsPath = path.join(cwd, 'site.js');
|
||||
|
||||
if (!await exists(manifestPath)) {
|
||||
console.warn('[harmony.patch] manifest.json 不存在, 跳过 privacy 同步');
|
||||
return { written: false };
|
||||
}
|
||||
if (!await exists(appJsonPath)) {
|
||||
console.warn(`[harmony.patch] app.json 不存在 (${label || appJsonPath}), 跳过 privacy 同步`);
|
||||
return { written: false };
|
||||
}
|
||||
|
||||
// 1. 读 manifest.json, 提取 mp-harmony.privacy 配置 (含占位符)
|
||||
const manifestRaw = await fsp.readFile(manifestPath, 'utf8');
|
||||
const manifestClean = stripJsonComments(manifestRaw);
|
||||
const manifest = JSON.parse(manifestClean);
|
||||
const privacyConfigTemplate = manifest && manifest['mp-harmony'] && manifest['mp-harmony'].privacy;
|
||||
|
||||
if (!privacyConfigTemplate) {
|
||||
return { written: false };
|
||||
}
|
||||
|
||||
// 2. 读 site.js, 提取真实 baseUrl + uniacid
|
||||
if (!await exists(siteJsPath)) {
|
||||
console.warn('[harmony.patch] site.js 不存在, 跳过 privacy 同步');
|
||||
return { written: false };
|
||||
}
|
||||
const siteJsSource = await fsp.readFile(siteJsPath, 'utf8');
|
||||
const siteConfigRaw = parseSiteConfig(siteJsSource);
|
||||
if (!siteConfigRaw) {
|
||||
console.warn('[harmony.patch] ⚠️ site.js 解析失败 (找不到 baseUrl / uniacid), 跳过 privacy 同步');
|
||||
return { written: false };
|
||||
}
|
||||
const siteConfig = {
|
||||
baseUrl: normalizeBaseUrlForPlaceholder(siteConfigRaw.baseUrl),
|
||||
uniacid: siteConfigRaw.uniacid,
|
||||
};
|
||||
|
||||
// 3. 占位符替换
|
||||
const privacyConfig = replacePlaceholders(privacyConfigTemplate, siteConfig)
|
||||
|
||||
// 4. 校验替换结果
|
||||
const finalStr = JSON.stringify(privacyConfig)
|
||||
if (/YOUR_DOMAIN|YOUR_UNIACID/.test(finalStr)) {
|
||||
console.warn('[harmony.patch] ⚠️ 替换后仍含占位符, 跳过写入. 请检查 site.js / manifest.json 配置')
|
||||
return { written: false };
|
||||
}
|
||||
|
||||
// 5. 写目标 app.json
|
||||
const appJsonRaw = await fsp.readFile(appJsonPath, 'utf8');
|
||||
const appJson = JSON.parse(appJsonRaw);
|
||||
appJson.privacy = privacyConfig;
|
||||
await fsp.writeFile(appJsonPath, JSON.stringify(appJson, null, 2) + '\n', 'utf8');
|
||||
const labelSuffix = label ? ` (${label})` : '';
|
||||
console.log(`[harmony.patch] privacy 配置已同步到 app.json${labelSuffix} (baseUrl=${siteConfig.baseUrl}, uniacid=${siteConfig.uniacid})`);
|
||||
|
||||
// 6. 写后验证
|
||||
await verifyWrittenAppJson(appJsonPath, siteConfig)
|
||||
return { written: true, siteConfig }
|
||||
} catch (err) {
|
||||
console.error(`[harmony.patch] 同步 privacy 配置失败 (${label || appJsonPath}):`, err);
|
||||
process.exitCode = 1;
|
||||
return { written: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步 privacy 到 stub ascf_src/app.json (兼容旧调用)
|
||||
*/
|
||||
async function syncPrivacyToStub() {
|
||||
const cwd = path.join(__dirname, '..');
|
||||
const stubAppJsonPath = path.join(cwd, 'stub-app-projects', 'mp-harmony', 'ascf', 'ascf_src', 'app.json');
|
||||
return syncPrivacyToPath(stubAppJsonPath, 'stub');
|
||||
}
|
||||
|
||||
async function copyAssets(mode = 'production') {
|
||||
try {
|
||||
const cwd = path.join(__dirname, '..');
|
||||
@@ -437,8 +96,7 @@ async function main() {
|
||||
const argv = process.argv.slice(2);
|
||||
const options = {
|
||||
noZip: argv.includes('--no-zip'),
|
||||
mode: 'production',
|
||||
syncPrivacyOnly: argv.includes('--sync-privacy-only'),
|
||||
mode: 'production'
|
||||
};
|
||||
|
||||
const modeIndex = argv.indexOf('--mode');
|
||||
@@ -446,46 +104,18 @@ async function main() {
|
||||
options.mode = argv[modeIndex + 1];
|
||||
}
|
||||
|
||||
// 子命令: --sync-privacy-only
|
||||
// vue.config.js _copyMpHarmonyBuild() 后, 构建产物的 app.json
|
||||
// 会覆盖 stub ascf_src/app.json (因为产物里 app.json 含占位符)。
|
||||
// vue.config.js 此时再 exec 这个子命令, 强制重新同步一次。
|
||||
if (options.syncPrivacyOnly) {
|
||||
console.log('[harmony.patch] --sync-privacy-only: 重新同步 privacy (覆盖 _copyMpHarmonyBuild 的拷贝)');
|
||||
await syncPrivacyToStub();
|
||||
return;
|
||||
}
|
||||
|
||||
// 同步 site.js 到 stub 项目源码目录,确保编译时能动态导入
|
||||
await syncSiteToStub(options.mode);
|
||||
|
||||
// 同步 manifest.json 的 mp-harmony.privacy 到 stub app.json (防止上架时 URL 不同步)
|
||||
await syncPrivacyToStub();
|
||||
|
||||
await copyAssets(options.mode);
|
||||
|
||||
// 关键: 在打 ZIP 之前, 同步一次 **构建产物** 的 app.json
|
||||
// HBuilderX 构建产物的 app.json 含占位符, 不修正就直接打 ZIP,
|
||||
// 发行包里的 app.json 还是占位符, 上架会被审核驳回。
|
||||
// 这步必须在 ZIP 之前, 必须在 _copyMpHarmonyBuild 之后 (vue.config.js 顺序)。
|
||||
if (!options.noZip && options.mode === 'production') {
|
||||
const cwd = path.join(__dirname, '..');
|
||||
const buildAppJsonPath = path.join(cwd, 'unpackage', 'dist', 'build', '.mp-harmony', 'app.json');
|
||||
if (await exists(buildAppJsonPath)) {
|
||||
console.log('[harmony.patch] 同步构建产物的 app.json (打 ZIP 前)');
|
||||
await syncPrivacyToPath(buildAppJsonPath, 'build');
|
||||
} else {
|
||||
console.warn(`[harmony.patch] 构建产物的 app.json 不存在: ${buildAppJsonPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.noZip && options.mode === 'production') {
|
||||
const cwd = path.join(__dirname, '..');
|
||||
const sourceDir = path.join(cwd, 'unpackage', 'dist', 'build', '.mp-harmony');
|
||||
const destDir = path.join(cwd, 'unpackage', 'dist', 'build');
|
||||
const zipFilePath = await createZipWithSystemCommand(sourceDir, destDir, 'mp-harmony');
|
||||
console.log(`ZIP 文件路径: ${zipFilePath}`);
|
||||
|
||||
|
||||
await openFileDirectory(zipFilePath);
|
||||
} else {
|
||||
console.log('跳过创建 ZIP 文件和打开目录');
|
||||
|
||||
@@ -1,77 +1,14 @@
|
||||
{
|
||||
"bundle_version": "1784767380",
|
||||
"bundle_version": "1782901586",
|
||||
"bundle_lang": "zh-cn",
|
||||
"bundle_leaves": 2659,
|
||||
"code_calls_total": 2693,
|
||||
"code_unique_keys": 2097,
|
||||
"found_in_bundle": 2086,
|
||||
"bundle_leaves": 2649,
|
||||
"code_calls_total": 2677,
|
||||
"code_unique_keys": 2083,
|
||||
"found_in_bundle": 2082,
|
||||
"missing_summary": {
|
||||
"TRULY_MISSING": 12,
|
||||
"SYNTAX_BUG": 1
|
||||
},
|
||||
"missing_classified": {
|
||||
"TRULY_MISSING": [
|
||||
{
|
||||
"key": "common.buyNow",
|
||||
"file": "components\\ns-goods-recommend\\ns-goods-recommend.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "wechatChannel.wechatMpOnly",
|
||||
"file": "components-diy\\diy-channel-list.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "common.submitSuccess",
|
||||
"file": "components-diy\\diy-form.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "submitFail",
|
||||
"file": "components-diy\\diy-form.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "diy.networkError",
|
||||
"file": "pages\\index\\index.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "versionInfo",
|
||||
"file": "pages\\index\\public\\js\\index.js",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "commitId",
|
||||
"file": "pages\\index\\public\\js\\index.js",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "buildTime",
|
||||
"file": "pages\\index\\public\\js\\index.js",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "diy.networkError",
|
||||
"file": "pages_goods\\category.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "goods.detail.formEmptyTip",
|
||||
"file": "pages_goods\\detail.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "common.buyNow",
|
||||
"file": "pages_goods\\public\\js\\list.js",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "pay.index.payInfoNotFound",
|
||||
"file": "pages_tool\\pay\\index.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
}
|
||||
],
|
||||
"SYNTAX_BUG": [
|
||||
{
|
||||
"key": "order.list.status.",
|
||||
@@ -82,8 +19,18 @@
|
||||
},
|
||||
"placeholder_issues": [],
|
||||
"data_lang_issues": {
|
||||
"components\\ns-payment\\ns-payment.vue": [
|
||||
"components.nsPayment.alipay"
|
||||
],
|
||||
"pages_promotion\\fenxiao\\apply.vue": [
|
||||
"fenxiao.apply.pleaseEnterName"
|
||||
],
|
||||
"pages_promotion\\merch\\public\\js\\list.js": [
|
||||
"merch.list.buyText"
|
||||
],
|
||||
"pages_tool\\ai-chat\\ai-chat-message.vue": [
|
||||
"ai-chat.index.loadMoreHistory",
|
||||
"ai-chat.index.defaultNickname"
|
||||
]
|
||||
},
|
||||
"hardcoded_navbar_titles": [
|
||||
@@ -174,11 +121,6 @@
|
||||
"common.tabBar.cart",
|
||||
"common.tabBar.member"
|
||||
],
|
||||
"local_only_keys": [
|
||||
"buildTime",
|
||||
"commitId",
|
||||
"submitFail",
|
||||
"versionInfo"
|
||||
],
|
||||
"local_only_keys": [],
|
||||
"bundle_only_keys_in_unique": []
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# 精筛 v2:剥离 HTML 注释和 JS 注释后,再扫硬编码中文(只关注真实会被渲染的)
|
||||
import os, re, json
|
||||
from collections import defaultdict
|
||||
|
||||
ROOT = r'D:\projects\shop-projects\frontend'
|
||||
SKIP_DIR = {'node_modules', '.unpackage', 'unpackage', '.git', 'dist', 'stub-app-projects', 'lang', 'tests', 'docs', 'scripts', 'common/mixin'}
|
||||
|
||||
CN = re.compile(r'[\u4e00-\u9fff]')
|
||||
LANG_RE = re.compile(r'\$lang\(')
|
||||
|
||||
TPL_RE = re.compile(r'<template[^>]*>([\s\S]*?)</template>', re.I)
|
||||
TAG_RE = re.compile(r'<([a-zA-Z][\w-]*)\b[^>]*>([^<>\n]+)</\1>')
|
||||
HTML_COMMENT_RE = re.compile(r'<!--[\s\S]*?-->')
|
||||
|
||||
JS_TRIGGERS = [
|
||||
re.compile(r'uni\.show(?:Toast|Modal|ActionSheet|Loading)\s*\(\s*\{[\s\S]*?\}\s*\)', re.S),
|
||||
re.compile(r'wx\.show(?:Toast|Modal|ActionSheet|Loading)\s*\(\s*\{[\s\S]*?\}\s*\)', re.S),
|
||||
re.compile(r'\btitle\s*:\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
re.compile(r'\bcontent\s*:\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
re.compile(r'\btext\s*:\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
re.compile(r'\bplaceholder\s*:\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
re.compile(r'\balert\s*\(\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
re.compile(r'\bconfirm\s*\(\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
]
|
||||
|
||||
def strip_all_comments(text, is_vue=False):
|
||||
text = HTML_COMMENT_RE.sub(lambda m: ' ' * len(m.group()), text)
|
||||
text = re.sub(r'/\*[\s\S]*?\*/', lambda m: ' ' * len(m.group()), text)
|
||||
text = re.sub(r'//[^\n]*', lambda m: ' ' * len(m.group()), text)
|
||||
return text
|
||||
|
||||
results = defaultdict(list)
|
||||
|
||||
for dirpath, dirnames, filenames in os.walk(ROOT):
|
||||
if any(s in dirpath.replace('\\', '/') for s in SKIP_DIR):
|
||||
continue
|
||||
for fn in filenames:
|
||||
if not fn.endswith(('.vue', '.js')):
|
||||
continue
|
||||
fp = os.path.join(dirpath, fn)
|
||||
rel = os.path.relpath(fp, ROOT).replace('\\', '/')
|
||||
try:
|
||||
content = open(fp, 'r', encoding='utf-8').read()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
stripped = strip_all_comments(content)
|
||||
|
||||
if fn.endswith('.vue'):
|
||||
for m in TPL_RE.finditer(stripped):
|
||||
tpl = m.group(1)
|
||||
for tag in TAG_RE.finditer(tpl):
|
||||
inner = tag.group(2).strip()
|
||||
if CN.search(inner) and not LANG_RE.search(inner):
|
||||
line_no = stripped[:m.start(1) + tpl.find(inner)].count('\n') + 1
|
||||
results[rel].append({'kind': 'template', 'tag': tag.group(1), 'text': inner[:160], 'line': line_no})
|
||||
|
||||
for trig in JS_TRIGGERS:
|
||||
for m in trig.finditer(stripped):
|
||||
text = (m.group(1) if m.lastindex else m.group(0))
|
||||
if not CN.search(text):
|
||||
continue
|
||||
if LANG_RE.search(text):
|
||||
continue
|
||||
line_no = stripped[:m.start()].count('\n') + 1
|
||||
results[rel].append({'kind': 'js-text', 'match': text[:160], 'line': line_no})
|
||||
|
||||
ranked = sorted(results.items(), key=lambda kv: -len(kv[1]))
|
||||
print(f'total files: {len(ranked)}')
|
||||
print(f'total hits: {sum(len(v) for v in ranked)}')
|
||||
|
||||
out = []
|
||||
for f, hits in ranked[:60]:
|
||||
out.append({'file': f, 'hits': len(hits), 'samples': hits[:3]})
|
||||
print(json.dumps(out, ensure_ascii=False, indent=2))
|
||||
|
||||
with open(os.path.join(ROOT, 'scripts', 'hardcoded_cn_real.json'), 'w', encoding='utf-8') as f:
|
||||
json.dump({k: v for k, v in ranked}, f, ensure_ascii=False, indent=2)
|
||||
print('saved hardcoded_cn_real.json')
|
||||
4
site.js
4
site.js
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
baseUrl: "https://5g.aigc-quickapp.com/",//修改域名
|
||||
uniacid: 248,//后台对应uniacid
|
||||
baseUrl: "https://xcx2.aigc-quickapp.com/",//修改域名
|
||||
uniacid: 2482,//后台对应uniacid
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.atomicservice.6917610986330791161",
|
||||
"bundleName": "com.atomicservice.6917600524822452448",
|
||||
"bundleType": "atomicService",
|
||||
"vendor": "example",
|
||||
"versionCode": 1000000,
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"buildOptionSet": [
|
||||
{
|
||||
"name": "release",
|
||||
"arkOptions": {
|
||||
"obfuscation": {
|
||||
"ruleOptions": {
|
||||
"enable": true,
|
||||
"files": [
|
||||
"./obfuscation-rules.txt"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { hspTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: hspTasks,
|
||||
plugins: []
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
# Define project specific obfuscation rules here.
|
||||
# You can include the obfuscation configuration files in the current module's build-profile.json5.
|
||||
#
|
||||
# For more details, see
|
||||
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
|
||||
|
||||
# Obfuscation options:
|
||||
# -disable-obfuscation: disable all obfuscations
|
||||
# -enable-property-obfuscation: obfuscate the property names
|
||||
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
|
||||
# -compact: remove unnecessary blank spaces and all line feeds
|
||||
# -remove-log: remove all console.* statements
|
||||
# -print-namecache: print the name cache that contains the mapping from the old names to new names
|
||||
# -apply-namecache: reuse the given cache file
|
||||
|
||||
# Keep options:
|
||||
# -keep-property-name: specifies property names that you want to keep
|
||||
# -keep-global-name: specifies names that you want to keep in the global scope
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "components_diy",
|
||||
"version": "1.0.0",
|
||||
"description": "Please describe the basic information.",
|
||||
"main": "Index.ets",
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"packageType": "InterfaceHar",
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "components_diy",
|
||||
"type": "shared",
|
||||
"description": "subpackage",
|
||||
"deviceTypes": [
|
||||
"phone"
|
||||
],
|
||||
"deliveryWithInstall": true,
|
||||
"installationFree": true
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"name": "shared_desc",
|
||||
"value": "description"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"buildOptionSet": [
|
||||
{
|
||||
"name": "release",
|
||||
"arkOptions": {
|
||||
"obfuscation": {
|
||||
"ruleOptions": {
|
||||
"enable": true,
|
||||
"files": [
|
||||
"./obfuscation-rules.txt"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { hspTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: hspTasks,
|
||||
plugins: []
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
# Define project specific obfuscation rules here.
|
||||
# You can include the obfuscation configuration files in the current module's build-profile.json5.
|
||||
#
|
||||
# For more details, see
|
||||
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
|
||||
|
||||
# Obfuscation options:
|
||||
# -disable-obfuscation: disable all obfuscations
|
||||
# -enable-property-obfuscation: obfuscate the property names
|
||||
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
|
||||
# -compact: remove unnecessary blank spaces and all line feeds
|
||||
# -remove-log: remove all console.* statements
|
||||
# -print-namecache: print the name cache that contains the mapping from the old names to new names
|
||||
# -apply-namecache: reuse the given cache file
|
||||
|
||||
# Keep options:
|
||||
# -keep-property-name: specifies property names that you want to keep
|
||||
# -keep-global-name: specifies names that you want to keep in the global scope
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "pages_goods",
|
||||
"version": "1.0.0",
|
||||
"description": "Please describe the basic information.",
|
||||
"main": "Index.ets",
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"packageType": "InterfaceHar",
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "pages_goods",
|
||||
"type": "shared",
|
||||
"description": "subpackage",
|
||||
"deviceTypes": [
|
||||
"phone"
|
||||
],
|
||||
"deliveryWithInstall": true,
|
||||
"installationFree": true
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"name": "shared_desc",
|
||||
"value": "description"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"buildOptionSet": [
|
||||
{
|
||||
"name": "release",
|
||||
"arkOptions": {
|
||||
"obfuscation": {
|
||||
"ruleOptions": {
|
||||
"enable": true,
|
||||
"files": [
|
||||
"./obfuscation-rules.txt"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { hspTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: hspTasks,
|
||||
plugins: []
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
# Define project specific obfuscation rules here.
|
||||
# You can include the obfuscation configuration files in the current module's build-profile.json5.
|
||||
#
|
||||
# For more details, see
|
||||
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5
|
||||
|
||||
# Obfuscation options:
|
||||
# -disable-obfuscation: disable all obfuscations
|
||||
# -enable-property-obfuscation: obfuscate the property names
|
||||
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
|
||||
# -compact: remove unnecessary blank spaces and all line feeds
|
||||
# -remove-log: remove all console.* statements
|
||||
# -print-namecache: print the name cache that contains the mapping from the old names to new names
|
||||
# -apply-namecache: reuse the given cache file
|
||||
|
||||
# Keep options:
|
||||
# -keep-property-name: specifies property names that you want to keep
|
||||
# -keep-global-name: specifies names that you want to keep in the global scope
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "pages_order",
|
||||
"version": "1.0.0",
|
||||
"description": "Please describe the basic information.",
|
||||
"main": "Index.ets",
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"packageType": "InterfaceHar",
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "pages_order",
|
||||
"type": "shared",
|
||||
"description": "subpackage",
|
||||
"deviceTypes": [
|
||||
"phone"
|
||||
],
|
||||
"deliveryWithInstall": true,
|
||||
"installationFree": true
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"name": "shared_desc",
|
||||
"value": "description"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
},
|
||||
"buildOptionSet": [
|
||||
{
|
||||
"name": "release",
|
||||
"arkOptions": {
|
||||
"obfuscation": {
|
||||
"ruleOptions": {
|
||||
"enable": true,
|
||||
"files": [
|
||||
"./obfuscation-rules.txt"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { hspTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
|
||||
export default {
|
||||
system: hspTasks,
|
||||
plugins: []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user