Compare commits

...

45 Commits

Author SHA1 Message Date
695d27c5cb chore: merge dev/1.0 into main
包含以下修复:
- ai-chat 键盘弹出、输入框闪烁、滚动、WebSocket 等问题
- 小米微信小程序魔方组件布局修复
- 三处 calc() 精度统一避免小米子像素舍入差异
- H5 构建配置优化
2026-05-14 08:43:45 +08:00
cd82ba2f27 fix: 统一 calc() 精度避免小米微信小程序子像素舍入差异
- diy-video-list.vue, diy-goods-list.vue: 删除重复的 3n+3 规则
  (33.33% 精度低于 33.3333333%,且与 3n 规则匹配同一组元素)
- diy-fenxiao-goods-list.vue: 两列布局统一 calc(50% - 10rpx),
  消除第2项 1rpx 的减数差异
2026-05-14 08:42:12 +08:00
698d419525 fix(diy-rubik-cube): 小米微信小程序魔方组件布局修复
- 取消注释 .template-left/.template-right 的 width: 50%,修复小米
  XWeb 引擎 flex-wrap 子像素舍入差异导致的布局错乱
- 重构 calcRowOneLeftOfTwoRight:将 'px' 追加移出主循环,统一在
  第二层循环处理,避免中途字符串转换
- 清理 calcRowOneLeftOfOneTopOfTwoBottom 中的无效空值表达式
- 优化 row1-lt-of2-rt 模板右侧最后一项条件判断 marginBottom
2026-05-13 18:23:56 +08:00
a252d42b62 fix(ai-chat): 消除键盘弹出时输入框闪烁
- 键盘垫高层不再使用 v-if 突然插入,改为始终存在(高度默认为0)
- 添加 CSS transition: height 0.25s ease 平滑过渡动画
2026-05-13 16:38:33 +08:00
69466f410c fix(ai-chat): 修复聊天页面键盘弹出时导航栏上移、输入框不可见、间距问题
- 自定义导航栏 position:absolute 固定在顶部,不随键盘上移
- textarea 设置 adjust-position=false,阻止系统上推页面
- 输入框后方加 spacer 垫高层,键盘弹出时将输入框推至键盘上方
- chat-content 根据平台设置不同 top 值(MP:0 / H5:44px)
- 去掉输入框多余的安全区域内边距
- scroll-view 底部 padding 随键盘高度动态调整
2026-05-13 16:01:15 +08:00
39245dfcac fix(ai-chat): 修复输入框定位及滚动问题并更新客服名称 2026-05-13 14:08:58 +08:00
80b18cee0f feat(ai-chat): 优化加载状态动画与流式消息过渡 2026-05-13 09:19:38 +08:00
b2688dc8a1 fix(ai-chat): 修复历史消息加载逻辑及超时问题 2026-05-13 09:00:20 +08:00
d5ca77e2d8 fix(ai-chat): 跳过健康检查避免服务误判 2026-05-13 08:40:01 +08:00
e789ab2ca3 fix(H5): 从 package.json 读取 UNI_APP_DOMAIN 兜底,确保 H5 发行域名正常注入 2026-05-12 11:39:43 +08:00
5e83771193 chore: 保留H5打包后的源目录 2026-05-12 09:59:43 +08:00
0d18041037 refactor: 使用 Intl.DateTimeFormat 生成时间戳 2026-05-12 08:48:55 +08:00
875cf0b428 feat(ai-chat): 增强Markdown解析及调整聊天气泡布局 2026-05-12 08:30:26 +08:00
e16402c9c1 fix(ai): 修复WebSocket重复回调及意外关闭问题 2026-05-11 17:14:26 +08:00
dee2887026 docs: 更新 readme 反映 H5 自动 ZIP 打包和统一配置方式 2026-05-11 11:56:37 +08:00
6033cc2fa0 feat(vue.config): H5 生产构建后自动打包 ZIP 2026-05-11 10:59:36 +08:00
c1fbabe07b refactor(vue.config): 简化UNI_OUTPUT_DIR逻辑,支持通用H5脚本+读取manifest base 2026-05-11 10:54:36 +08:00
221ef7bb6d refactor: 域名移至package.json脚本env中,config.js零改动 2026-05-11 10:29:33 +08:00
55c1e80ed7 refactor: 用 UNI_SCRIPT + DOMAIN_MAP 替代 #ifdef 环境常量 2026-05-11 10:27:33 +08:00
79202c4542 docs: 更新 package.json 和 readme.md 反映最新构建流程 2026-05-11 10:18:47 +08:00
3bff91e421 refactor: 统一版本信息注入方式
- feat(vue.config): 新增 VersionInjectPlugin,在 webpack emit 钩子中内联注入 __app_version 到 vendor.js / index.html
- refactor(scripts): mp-weixin.patch.js 精简为仅拷贝 site.js + ZIP 打包
- feat(index): 首页连击5次显示构建版本信息
- fix(util): 修复 openWxWorkServiceChat fail 回调语法错误
2026-05-11 09:36:57 +08:00
979384c2d6 feat(index): 新增固定版本触发区,标题不可见时也可连击5次查看版本 2026-05-09 17:27:53 +08:00
504e5322fb fix(scripts): vendor.js注入改为先清理旧行再注入,确保幂等 2026-05-09 17:21:02 +08:00
25fc0a59a8 fix(scripts): version.json改为生成version.js(CommonJS),兼容微信小程序require限制 2026-05-09 17:19:17 +08:00
34e27d0621 feat(index): 首页标题连击5次显示构建版本信息\nfix(scripts): explorer使用spawn避免GUI进程阻塞\nfix(index): version.json运行时读取,不依赖构建时require 2026-05-09 17:14:51 +08:00
913fc90176 feat(scripts): 构建时自动生成 version.json 并注入到 vendor.js 2026-05-09 17:04:12 +08:00
c1bac6d9e8 feat(scripts): ZIP文件名使用Git提交ID替换时间戳 2026-05-09 17:00:38 +08:00
31382a2388 refactor(util): 增加空回调函数 2026-05-09 11:16:30 +08:00
d009077183 update 2026-05-09 09:24:25 +08:00
133eb29a71 Merge branch 'dev/1.0' 2026-05-09 09:22:10 +08:00
4e480d35df Merge branch 'Branch_release/v1.4.1' 2026-04-09 15:54:51 +08:00
bc6d243362 chore: 2026-04-09 15:30:32 +08:00
a285706112 chore:用户隐私协议点击不同意后不会在弹出了 2026-04-09 14:56:49 +08:00
9c04c031ab chore:删除原来的多余的调动弹窗的代码 2026-04-09 10:40:43 +08:00
0ce929f8a1 chore:点击同意后,用户隐私不在弹出 2026-04-09 10:04:29 +08:00
61ab9f11bd chore:H5只弹一次,点击同意后就不弹了 2026-04-09 09:35:08 +08:00
2747cb49ae chore:弹出用户隐私 2026-04-08 18:20:03 +08:00
9c8ebf8d74 Merge branch 'main' of https://git.aigc-quickapp.com/Uniapp/lucky_shop 2026-04-08 15:02:08 +08:00
e19fef785f chore:改了commom 2026-04-08 14:39:23 +08:00
92ae114c5c chore: 2026-04-08 11:47:07 +08:00
b25c366a5b chore:增加了弹窗用户隐私 2026-04-08 11:37:29 +08:00
b23c21d05b Merge pull request 'fix/lang' (#4) from fix/lang into main
Reviewed-on: #4
2026-02-04 09:12:58 +00:00
65a24f25bb fix(lang): 提取getCurrentLocale为独立函数并优化代码格式
将getCurrentLocale方法提取为独立函数以避免重复代码
移除多余的空格并保持代码风格一致
2026-02-04 17:10:12 +08:00
2a0935b581 Merge pull request 'docs: 更新readme文档结构和内容' (#2) from dev/1.0 into main
Reviewed-on: #2
2026-01-24 09:41:36 +00:00
b31197a8b4 Merge pull request 'dev/1.0' (#1) from dev/1.0 into main
Reviewed-on: #1
2026-01-24 09:24:05 +00:00
21 changed files with 1189 additions and 647 deletions

View File

@@ -42,18 +42,25 @@ const localDevConfig = ({
uniacid: 2, uniacid: 2,
domain: 'http://localhost:8050/', domain: 'http://localhost:8050/',
}, },
'100': { '100': {
uniacid: 100, uniacid: 100,
domain: 'https://5g.aigc-quickapp.com/', domain: 'https://5g.aigc-quickapp.com/',
}, },
'1': { '1': {
uniacid: 1, uniacid: 1,
domain: 'https://5g.aigc-quickapp.com/', domain: 'https://5g.aigc-quickapp.com/',
}, },
'115': { '115': {
uniacid: 115, uniacid: 115,
domain: 'https://5g.aigc-quickapp.com/', domain: 'https://5g.aigc-quickapp.com/',
}, },
})['2811']; // 选择要使用的环境配置 '47': { // 修改弹窗
uniacid: 47,
domain: 'https://5g.aigc-quickapp.com/',
},
'2075': { //
uniacid: 2075,
domain: 'https://xcx.aigc-quickapp.com/',
},
})['1']; // 选择要使用的环境配置
export default localDevConfig; export default localDevConfig;

View File

@@ -68,7 +68,7 @@ export default {
} }
return result; return result;
} catch (error) { } catch (error) {
console.error('[AI Service] Dify API请求失败,详细错误:', error); console.error('[AI Service] AiChat API请求失败,详细错误:', error);
console.error('[AI Service] 错误堆栈:', error.stack); console.error('[AI Service] 错误堆栈:', error.stack);
// 打印更多信息帮助调试 // 打印更多信息帮助调试
console.error('[AI Service] store 状态:', { console.error('[AI Service] store 状态:', {
@@ -88,6 +88,16 @@ export default {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const match = Config.baseUrl.match(/^(?:https?:\/\/|wss?:\/\/)([^\/\s:]+)/); const match = Config.baseUrl.match(/^(?:https?:\/\/|wss?:\/\/)([^\/\s:]+)/);
const domain = match ? match[1] : ''; const domain = match ? match[1] : '';
// 修复 5:Domain 提取失败时提前拒绝
if (!domain) {
const errorMsg = 'WebSocket 域名配置无效';
console.error(errorMsg);
reject(new Error(errorMsg));
if (onComplete) onComplete({ error: errorMsg });
return;
}
const wssUrl = `wss://${domain}/ws/aikefu`; const wssUrl = `wss://${domain}/ws/aikefu`;
const socketTask = wx.connectSocket({ const socketTask = wx.connectSocket({
url: wssUrl, url: wssUrl,
@@ -97,6 +107,29 @@ export default {
let content = ''; let content = '';
let conversationId = ''; let conversationId = '';
let isAuthenticated = false; let isAuthenticated = false;
let closedNormally = false;
let settled = false;
// 安全地拒绝(防止重复调用)
function safeReject(error) {
if (settled) return;
settled = true;
clearTimeout(timeout);
reject(error);
}
// 安全地完成
function safeResolve(result) {
if (settled) return;
settled = true;
clearTimeout(timeout);
resolve(result);
}
// 安全地触发 onComplete
function safeComplete(result) {
if (onComplete) onComplete(result);
}
socketTask.onOpen(() => { socketTask.onOpen(() => {
socketTask.send({ socketTask.send({
@@ -128,8 +161,8 @@ export default {
} else if (data.type === 'auth_failed') { } else if (data.type === 'auth_failed') {
const errorMsg = '认证失败,请重新登录'; const errorMsg = '认证失败,请重新登录';
console.error(errorMsg, data); console.error(errorMsg, data);
reject(new Error(errorMsg)); safeComplete({ error: errorMsg });
if (onComplete) onComplete({ error: errorMsg }); safeReject(new Error(errorMsg));
socketTask.close(); socketTask.close();
} }
@@ -146,10 +179,9 @@ export default {
if (conversationId) { if (conversationId) {
this.setConversationId(conversationId); this.setConversationId(conversationId);
} }
if (onComplete) { closedNormally = true;
onComplete({ content, conversation_id: conversationId }); safeComplete({ content, conversation_id: conversationId });
} safeResolve({ content, conversation_id: conversationId });
resolve({ content, conversation_id: conversationId });
socketTask.close(); socketTask.close();
} }
@@ -165,21 +197,19 @@ export default {
socketTask.onError((err) => { socketTask.onError((err) => {
const errorMsg = 'WebSocket 连接失败'; const errorMsg = 'WebSocket 连接失败';
console.error(errorMsg, err); console.error(errorMsg, err);
reject(new Error(errorMsg)); safeComplete({ error: errorMsg });
if (onComplete) onComplete({ error: errorMsg }); safeReject(new Error(errorMsg));
}); });
socketTask.onClose(() => { socketTask.onClose(() => {
if (!closedNormally && !settled) {
const errorMsg = 'WebSocket 连接意外关闭';
console.error(errorMsg);
safeComplete({ error: errorMsg });
safeReject(new Error(errorMsg));
}
}); });
const timeout = setTimeout(() => {
if (!isAuthenticated || content === '') {
console.warn('WebSocket 超时,强制关闭');
socketTask.close();
reject(new Error('AI服务响应超时'));
if (onComplete) onComplete({ error: 'AI服务响应超时' });
}
}, 10000);
}); });
// #endif // #endif
}, },

View File

@@ -18,22 +18,15 @@ try {
// 线上版本部署域名, 注意: 必须以 '/' 结尾,适用于小程序/H5/快应用等场景 // 线上版本部署域名, 注意: 必须以 '/' 结尾,适用于小程序/H5/快应用等场景
// 默认域名, 自定义发行时可以修改 // 默认域名, 自定义发行时可以修改
let defaultDomain = ''; let defaultDomain = '';
// #ifdef H5_XCX_5G_QUICKAPP_COM // #ifdef H5
defaultDomain = 'https://xcx20.5g-quickapp.com'; // 域名由 vue.config.js 中 DefinePlugin 注入的 process.env.UNI_APP_DOMAIN 传入
// #endif try {
if (process.env.UNI_APP_DOMAIN) {
// #ifdef H5_XCX_AIGC_QUICKAPP_COM defaultDomain = process.env.UNI_APP_DOMAIN;
defaultDomain = 'https://xcx.aigc-quickapp.com/'; }
// #endif } catch (e) {}
// #ifdef H5_5G_AIGC_QUICKAPP_COM
defaultDomain = 'https://5g.aigc-quickapp.com/';
// #endif
// #ifdef H5_TEST_AIGC_QUICKAPP_COM
defaultDomain = 'https://test.aigc-quickapp.com/';
// #endif // #endif
// 确保域名以 '/' 结尾 // 确保域名以 '/' 结尾

View File

@@ -119,9 +119,9 @@ export class CustomerService {
} }
/** /**
* 跳转到 AI 客服页面(Dify) * 跳转到 AI智能客服页面(AiChat)
*/ */
openDifyService() { openAiChatService() {
try { try {
// 清除未读数(如果存在) // 清除未读数(如果存在)
if (typeof this.vm.setAiUnreadCount === 'function') { if (typeof this.vm.setAiUnreadCount === 'function') {
@@ -135,7 +135,7 @@ export class CustomerService {
uni.navigateTo({ uni.navigateTo({
url: aiChatUrl, url: aiChatUrl,
fail: (err) => { fail: (err) => {
console.error('跳转 AI 客服失败:', err); console.error('跳转 AI智能客服失败:', err);
// H5 兜底 // H5 兜底
// #ifdef H5 // #ifdef H5
window.location.href = aiChatUrl; window.location.href = aiChatUrl;
@@ -144,7 +144,7 @@ export class CustomerService {
} }
}); });
} catch (e) { } catch (e) {
console.error('跳转 AI 客服异常:', e); console.error('跳转 AI智能客服异常:', e);
uni.showToast({ title: '打开客服失败', icon: 'none' }); uni.showToast({ title: '打开客服失败', icon: 'none' });
} }
} }
@@ -177,7 +177,7 @@ export class CustomerService {
// 核心路由:根据 type 决定行为 // 核心路由:根据 type 决定行为
switch (config.type) { switch (config.type) {
case 'aikefu': case 'aikefu':
this.openDifyService(); this.openAiChatService();
break; break;
case 'wxwork': case 'wxwork':
this.openWxworkService(false, config, options); this.openWxworkService(false, config, options);
@@ -364,7 +364,7 @@ export class CustomerService {
openAliappService(config) { openAliappService(config) {
if (config.type === 'aikefu') { if (config.type === 'aikefu') {
this.openDifyService(); this.openAiChatService();
} else if (config.type === 'third') { } else if (config.type === 'third') {
this.openThirdService(config); this.openThirdService(config);
} else { } else {
@@ -565,7 +565,7 @@ export class CustomerService {
uni.navigateTo({ uni.navigateTo({
url: aiChatUrl, url: aiChatUrl,
fail: (err) => { fail: (err) => {
console.error('跳转 AI 客服失败:', err); console.error('跳转 AI智能客服失败:', err);
// #ifdef H5 // #ifdef H5
window.location.href = aiChatUrl; window.location.href = aiChatUrl;
// #endif // #endif
@@ -573,7 +573,7 @@ export class CustomerService {
} }
}); });
} catch (e) { } catch (e) {
console.error('跳转 AI 客服异常:', e); console.error('跳转 AI智能客服异常:', e);
uni.showToast({ title: '打开客服失败', icon: 'none' }); uni.showToast({ title: '打开客服失败', icon: 'none' });
} }
} }

View File

@@ -30,12 +30,12 @@ function processRoutePath(route) {
} }
} }
// ---- 处理页面目录映射 <end>---- // ---- 处理页面目录映射 <end>----
// 去掉pages目录,只保留子目录 // 去掉pages目录,只保留子目录
if (routeParts[0] === 'pages') { if (routeParts[0] === 'pages') {
routeParts = routeParts.slice(1); routeParts = routeParts.slice(1);
} }
return routeParts.join("/"); return routeParts.join("/");
} }
@@ -54,11 +54,14 @@ function loadLangPackSync(lang, path) {
} }
} }
/**
* 获得当前本地语言
* @returns
*/
function getCurrentLocale() { function getCurrentLocale() {
return uni.getStorageSync('lang') || "zh-cn"; return uni.getStorageSync('lang') || "zh-cn";
} }
export default { export default {
langList: langConfig.langList, langList: langConfig.langList,
@@ -77,18 +80,18 @@ export default {
if (!_page) return; if (!_page) return;
const locale = getCurrentLocale(); // 获得当前本地语言 const locale = getCurrentLocale(); // 获得当前本地语言
let value = ''; // 存放解析后的语言值 let value = ''; // 存放解析后的语言值
let langPath = ''; // 存放当前页面语言包路径 let langPath = ''; // 存放当前页面语言包路径
try { try {
//公共语言包(同步加载) //公共语言包(同步加载)
var lang = loadLangPackSync(locale, 'common'); var lang = loadLangPackSync(locale, 'common');
//当前页面语言包(同步加载) //当前页面语言包(同步加载)
let route = _page.route; let route = _page.route;
langPath = processRoutePath(route); langPath = processRoutePath(route);
// 加载当前页面语言包 // 加载当前页面语言包
let currentPageLang = loadLangPackSync(locale, langPath); let currentPageLang = loadLangPackSync(locale, langPath);
@@ -113,7 +116,7 @@ export default {
} else { } else {
value = mergedLang[field] !== undefined ? mergedLang[field] : field; value = mergedLang[field] !== undefined ? mergedLang[field] : field;
} }
} catch (e) { } catch (e) {
console.error('解析语言包失败:', e, { langPath, field, locale }); console.error('解析语言包失败:', e, { langPath, field, locale });
value = field; value = field;
@@ -129,7 +132,7 @@ export default {
// 多语言调试,注释后可以关闭控制台输出 // 多语言调试,注释后可以关闭控制台输出
if (field == value) { if (field == value) {
console.warn(`警告: 字段 ${field} 在语言包 ${langPath} 中未找到对应值,使用默认值 ${field} 当前语言: ${locale}`); console.warn(`警告: 字段 ${field} 在语言包 ${langPath} 中未找到对应值,使用默认值 ${field} 当前语言: ${locale}`);
} }
return value; return value;
@@ -165,7 +168,7 @@ export default {
if (!_page) return; if (!_page) return;
const locale = getCurrentLocale(); const locale = getCurrentLocale();
this.title(this.lang("title")); this.title(this.lang("title"));
//设置tabbar的文字语言 //设置tabbar的文字语言
@@ -215,4 +218,4 @@ export default {
} }
return list; return list;
} }
} }

View File

@@ -622,6 +622,7 @@ export default {
success: () => { success: () => {
}, },
fail: (err) => { fail: (err) => {
}
}); });
} else { } else {
fallbackFunc && fallbackFunc(); fallbackFunc && fallbackFunc();

View File

@@ -337,7 +337,7 @@ export default {
width: calc(50% - 10rpx); width: calc(50% - 10rpx);
&:nth-child(2n + 2) { &:nth-child(2n + 2) {
width: calc(50% - 11rpx); width: calc(50% - 10rpx);
margin-right: 0; margin-right: 0;
} }

View File

@@ -1018,10 +1018,6 @@ export default {
width: calc(33.3333333% - 14rpx); width: calc(33.3333333% - 14rpx);
box-sizing: border-box; box-sizing: border-box;
&:nth-child(3n + 3) {
width: calc(33.33% - 15rpx);
}
&:nth-of-type(1), &:nth-of-type(1),
&:nth-of-type(2), &:nth-of-type(2),
&:nth-of-type(3) { &:nth-of-type(3) {

View File

@@ -19,7 +19,7 @@
<template v-for="(item, index) in list"> <template v-for="(item, index) in list">
<template v-if="index > 0"> <template v-if="index > 0">
<view :key="index" :class="['item', value.mode]" @tap.stop="handlerClick(item.link, item.imageUrl)" <view :key="index" :class="['item', value.mode]" @tap.stop="handlerClick(item.link, item.imageUrl)"
:style="{ marginBottom: value.imageGap * 2 + 'rpx', width: item.imgWidth, height: item.imgHeight + 'px' }"> :style="{ marginBottom: index < list.length - 1 ? value.imageGap * 2 + 'rpx' : 0, width: item.imgWidth, height: item.imgHeight + 'px' }">
<image :src="$util.img(item.imageUrl)" :mode="item.imageMode || 'scaleToFill'" <image :src="$util.img(item.imageUrl)" :mode="item.imageMode || 'scaleToFill'"
:style="item.pageItemStyle" show-menu-by-longpress /> :style="item.pageItemStyle" show-menu-by-longpress />
</view> </view>
@@ -235,8 +235,6 @@ export default {
*/ */
calcRowOneLeftOfTwoRight() { calcRowOneLeftOfTwoRight() {
let rightHeight = 0; // 右侧两图平分高度 let rightHeight = 0; // 右侧两图平分高度
let divide = 'left'; // 划分规则,left:左,right:右
if (this.list[1].imgWidth === this.list[2].imgWidth) divide = 'right';
uni.getSystemInfo({ uni.getSystemInfo({
success: res => { success: res => {
this.list.forEach((item, index) => { this.list.forEach((item, index) => {
@@ -246,12 +244,14 @@ export default {
item.imgWidth = item.imgWidth / 2; item.imgWidth = item.imgWidth / 2;
item.imgHeight = item.imgWidth * ratio; item.imgHeight = item.imgWidth * ratio;
rightHeight = (item.imgHeight - uni.upx2px(this.value.imageGap * 2)) / 2; rightHeight = (item.imgHeight - uni.upx2px(this.value.imageGap * 2)) / 2;
item.imgWidth += 'px';
} else { } else {
item.imgWidth = this.list[0].imgWidth;
item.imgHeight = rightHeight; item.imgHeight = rightHeight;
} }
}); });
// 统一追加 px 单位
this.list.forEach((item) => {
item.imgWidth += 'px';
});
} }
}); });
}, },
@@ -311,7 +311,6 @@ export default {
this.list.forEach((item, index) => { this.list.forEach((item, index) => {
item.imgWidth += 'px'; item.imgWidth += 'px';
item.imgHeight;
}); });
} }
}); });
@@ -482,7 +481,7 @@ export default {
// 一左两右 // 一左两右
.rubik-cube .template-left, .rubik-cube .template-left,
.rubik-cube .template-right { .rubik-cube .template-right {
// width: 50%; width: 50%;
box-sizing: border-box; box-sizing: border-box;
} }

View File

@@ -230,10 +230,6 @@ export default {
width: calc(33.3333333% - 14rpx); width: calc(33.3333333% - 14rpx);
box-sizing: border-box; box-sizing: border-box;
&:nth-child(3n + 3) {
width: calc(33.33% - 15rpx);
}
&:nth-of-type(1), &:nth-of-type(1),
&:nth-of-type(2), &:nth-of-type(2),
&:nth-of-type(3) { &:nth-of-type(3) {

View File

@@ -1,9 +1,6 @@
{ {
"scripts": { "scripts": {
"mp-weixin": "node scripts/mp-weixin.patch.js", "mp-weixin": "node scripts/mp-weixin.patch.js"
"mp-weixin:patch": "node scripts/mp-weixin.patch.js --no-zip",
"mp-weixin:dev": "node scripts/mp-weixin.patch.js --mode development",
"mp-weixin:dev:patch": "node scripts/mp-weixin.patch.js --mode development --no-zip"
}, },
"devDependencies": { "devDependencies": {
"dart-sass": "^1.25.0", "dart-sass": "^1.25.0",
@@ -28,44 +25,40 @@
"h5-xcx20.5g-quickapp.com": { "h5-xcx20.5g-quickapp.com": {
"title": "H5-xcx20.5g-quickapp.com", "title": "H5-xcx20.5g-quickapp.com",
"env": { "env": {
"UNI_PLATFORM": "h5" "UNI_PLATFORM": "h5",
"UNI_APP_DOMAIN": "https://xcx20.5g-quickapp.com/"
}, },
"define": { "define": {
"H5_XCX_5G_QUICKAPP_COM": true,
"H5_PRODUCTION": true,
"PRODUCTION": true "PRODUCTION": true
} }
}, },
"h5-xcx.aigc-quickapp.com": { "h5-xcx.aigc-quickapp.com": {
"title": "H5-xcx.aigc-quickapp.com", "title": "H5-xcx.aigc-quickapp.com",
"env": { "env": {
"UNI_PLATFORM": "h5" "UNI_PLATFORM": "h5",
"UNI_APP_DOMAIN": "https://xcx.aigc-quickapp.com/"
}, },
"define": { "define": {
"H5_XCX_AIGC_QUICKAPP_COM": true,
"H5_PRODUCTION": true,
"PRODUCTION": true "PRODUCTION": true
} }
}, },
"h5-5g.aigc-quickapp.com": { "h5-5g.aigc-quickapp.com": {
"title": "H5-5g.aigc-quickapp.com", "title": "H5-5g.aigc-quickapp.com",
"env": { "env": {
"UNI_PLATFORM": "h5" "UNI_PLATFORM": "h5",
"UNI_APP_DOMAIN": "https://5g.aigc-quickapp.com/"
}, },
"define": { "define": {
"H5_5G_AIGC_QUICKAPP_COM": true,
"H5_PRODUCTION": true,
"PRODUCTION": true "PRODUCTION": true
} }
}, },
"h5-test.aigc-quickapp.com": { "h5-test.aigc-quickapp.com": {
"title": "H5-test.aigc-quickapp.com", "title": "H5-test.aigc-quickapp.com",
"env": { "env": {
"UNI_PLATFORM": "h5" "UNI_PLATFORM": "h5",
"UNI_APP_DOMAIN": "https://test.aigc-quickapp.com/"
}, },
"define": { "define": {
"H5_TEST_AIGC_QUICKAPP_COM": true,
"H5_PRODUCTION": true,
"PRODUCTION": true "PRODUCTION": true
} }
} }

View File

@@ -318,7 +318,7 @@
{ {
"path": "ai-chat/index", "path": "ai-chat/index",
"style": { "style": {
"navigationBarTitleText": "AI 客服" "navigationBarTitleText": "AI智能客服"
} }
}, },
{ {

View File

@@ -9,7 +9,7 @@
<image :src="$util.img(siteInfo.logo_square)" mode="aspectFill"></image> <image :src="$util.img(siteInfo.logo_square)" mode="aspectFill"></image>
</view> </view>
<view class="info-box" :style="{ color: '#ffffff' }"> <view class="info-box" :style="{ color: '#ffffff' }">
<text class="font-size-base">{{ siteInfo.site_name }}</text> <text class="font-size-base" @tap="onTitleTap">{{ siteInfo.site_name }}</text>
<text>{{ followOfficialAccount.welcomeMsg }}</text> <text>{{ followOfficialAccount.welcomeMsg }}</text>
</view> </view>
</view> </view>
@@ -141,6 +141,8 @@
<privacy-popup ref="privacyPopup"></privacy-popup> <privacy-popup ref="privacyPopup"></privacy-popup>
<to-top v-if="showTop" @toTop="scrollToTopNative()"></to-top> <to-top v-if="showTop" @toTop="scrollToTopNative()"></to-top>
<ns-login ref="login"></ns-login> <ns-login ref="login"></ns-login>
<!-- 版本信息触发区(固定定位,不可见,始终可点击) -->
<view class="version-trigger" @tap="onTitleTap"></view>
</view> </view>
</view> </view>
</template> </template>
@@ -243,4 +245,14 @@ export default {
bottom: 180rpx !important; bottom: 180rpx !important;
/* #endif */ /* #endif */
} }
.version-trigger {
position: fixed;
right: 0;
bottom: 50rpx;
width: 100rpx;
height: 100rpx;
z-index: 999;
background: transparent;
}
</style> </style>

View File

@@ -91,6 +91,39 @@ export default {
// 阻止事件继续传播 // 阻止事件继续传播
return false; return false;
},
/**
* 标题连击 5 次显示构建版本信息(供测试人员使用)
*/
onTitleTap() {
this._titleTapCount = (this._titleTapCount || 0) + 1;
if (this._titleTapTimer) {
clearTimeout(this._titleTapTimer);
}
if (this._titleTapCount >= 5) {
this._titleTapCount = 0;
this._showVersionInfo();
return;
}
// 2 秒内未达到 5 次则重置计数
this._titleTapTimer = setTimeout(() => {
this._titleTapCount = 0;
}, 2000);
},
_showVersionInfo() {
let v = null;
try {
v = typeof __app_version !== 'undefined' ? __app_version : null;
} catch (e) {}
if (v && v.commitId) {
uni.showModal({
title: '构建版本信息',
content: `提交 ID: ${v.commitId}\n构建时间: ${v.buildTime}`,
showCancel: false
});
} else {
console.log('未获取到构建版本信息');
}
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,19 @@
<template> <template>
<view class="container" :style="wrapperPageStyle"> <view class="wrapper">
<!--自定义导航头部 - 只在微信小程序显示,H5端隐藏 --> <!--自定义导航头部 - 绝对定位在顶部,不随键盘上移 -->
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<view class="custom-navbar" ref="pageHeader" v-if="showCustomNavbar"> <view class="custom-navbar" ref="pageHeader" v-if="showCustomNavbar">
<view class="header-left"> <view class="header-left">
<button class="back-btn" v-show="showBackButton" @click="goBack"> <button class="back-btn" v-show="showBackButton" @click="goBack">
<text class="iconfont icon-back"></text> <text class="iconfont icon-back"></text>
</button> </button>
<!-- 占位元素,确保布局平衡 -->
<view v-show="!showMenuButton" class="placeholder"></view> <view v-show="!showMenuButton" class="placeholder"></view>
</view> </view>
</view> </view>
<!-- #endif --> <!-- #endif -->
<!-- 聊天内容区域 --> <!-- 聊天内容区域:从导航栏下方到容器底部 -->
<view class="chat-content" <view class="chat-content">
:style="wrapperChatContentStyle"
>
<!-- AI聊天组件 -->
<ai-chat-message <ai-chat-message
ref="chat" ref="chat"
:initial-messages="initialMessages" :initial-messages="initialMessages"
@@ -131,37 +127,6 @@ export default {
return this.globalAIKefuConfig?.aiAvatar || this.$util.img(IMAGE_CONFIG.aiAvatar) return this.globalAIKefuConfig?.aiAvatar || this.$util.img(IMAGE_CONFIG.aiAvatar)
}, },
/// ---- others ---- /// ---- others ----
containerHeight() {
return `calc(100vh - ${this.navBarHeight + this.statusBarHeight}px)`
},
wrapperPageStyle() {
// #ifdef H5
return `top: $ {this.navBarHeight + 'px'};`
// #endif
// #ifdef MP-WEIXIN
return `top: -1px;` // 微信小程序需要上移1px, 否则与系统导航栏出现1px的空隙
// #endif
return ``
},
wrapperChatContentStyle() {
// #ifdef H5
// H5端没有自定义导航栏,不需要paddingTop
return {
height: this.containerHeight,
paddingTop: '0'
}
// #endif
// #ifdef MP-WEIXIN
// 微信小程序端根据导航栏显示情况决定
return {
height: this.containerHeight,
paddingTop: this.showCustomNavbar ? '0' : (this.navBarHeight + 'px')
}
// #endif
}
}, },
async onLoad(options) { async onLoad(options) {
@@ -574,20 +539,22 @@ export default {
@import url('/common/css/iconfont.css'); @import url('/common/css/iconfont.css');
/* 页面样式 */ /* 页面样式 */
.container { .wrapper {
display: flex; position: fixed;
flex-direction: column; top: 0;
background-color: white; /* 白色 */ left: 0;
overflow: hidden; right: 0;
position: fixed; bottom: 0;
top: 48px; overflow: hidden;
left: 0; }
right: 0;
bottom: 0;
}
/* 自定义导航头部 */ /* 自定义导航头部:固定在wrapper顶部,不随键盘上移 */
.custom-navbar { .custom-navbar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 88rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -667,12 +634,18 @@ export default {
} }
} }
/* 聊天内容区域 */ /* 聊天内容区域:从导航栏下方到容器底部 */
.chat-content { .chat-content {
flex: 1; position: absolute;
display: flex; /* #ifdef MP-WEIXIN */
flex-direction: column; top: 0;
overflow: hidden; /* #endif */
/* #ifndef MP-WEIXIN */
top: 44px;
/* #endif */
left: 0;
right: 0;
bottom: 0;
} }
/* 底部tabBar占位样式 */ /* 底部tabBar占位样式 */

119
readme.md
View File

@@ -40,22 +40,20 @@
1. 使用 HBuilderX 打开项目(不用改manifest.json的web,只有H5要改 ) 1. 使用 HBuilderX 打开项目(不用改manifest.json的web,只有H5要改 )
2. 选择菜单栏「发行」 → 「小程序-微信」→ 「发行」,等待构建完成 2. 选择菜单栏「发行」 → 「小程序-微信」→ 「发行」,等待构建完成
⚠️ **注意**:底部控制台弹出"请在微信小程序开发者工具中点击上传"后再执行下一步 ⚠️ **注意**:底部控制台弹出"请在微信小程序开发者工具中点击上传"后再执行下一步
3. 打开资源管理器→项目根目录,右键选择「在终端中打开」,执行命令: 3. 构建完成后,**自动执行**补丁脚本(拷贝 site.js / config 文件 + 生成 ZIP 包),无需手动操作
```bash
npm run mp-weixin
```
4. 找到项目根目录 `/unpackage/dist/build` 下生成的 mp-weixin 压缩包 4. 找到项目根目录 `/unpackage/dist/build` 下生成的 mp-weixin 压缩包
💡 **示例**:`mp-weixin-2026-01-23-1769152056146.zip` 💡 **示例**:`mp-weixin-2026-05-11-3bff91e.zip`
*压缩包文件名末尾为 Git 提交 ID*
*同时该目录下会生成未压缩的 `mp-weixin` 目录* *同时该目录下会生成未压缩的 `mp-weixin` 目录*
##### 4.1.1.3 【验证与重命名】 ##### 4.1.1.3 【验证与重命名】
1. 打开未压缩的 `mp-weixin` 目录,找到 `site.js` 文件,将文件内的 `uniacid` 值改为当前客户编号(如:2812)并保存 1. 打开未压缩的 `mp-weixin` 目录,找到 `site.js` 文件,将文件内的 `uniacid` 值改为当前客户编号(如:2812)并保存
2. 打开微信开发者工具,导入「mp-weixin」目录,点击「编译」,在开发者工具控制台验证有无报错,以及能否返回对应客户的业务数据 2. 打开微信开发者工具,导入「mp-weixin」目录,点击「编译」,在开发者工具控制台验证有无报错,以及能否返回对应客户的业务数据
3. 确保无误后将生成的 mp-weixin 压缩包重命名 3. 确保无误后将生成的 mp-weixin 压缩包重命名
- **定制化版**格式:`定制化-客户编号-域名-mp-weixin-当前日期-生成编号.zip` - **定制化版**格式:`定制化-客户编号-域名-mp-weixin-当前日期-Git提交ID.zip`
- **通用版**无需重命名 - **通用版**无需重命名
💡 **示例**:`custom-2812-xcx.aigc-quickapp.com-mp-weixin-2026-01-22-1769152056146.zip` 💡 **示例**:`custom-2812-xcx.aigc-quickapp.com-mp-weixin-2026-05-11-3bff91e.zip`
🚫 **禁止**:压缩包命名禁止包含 `/ \ : * ? " < > |` 等特殊字符 🚫 **禁止**:压缩包命名禁止包含 `/ \ : * ? " < > |` 等特殊字符
@@ -63,10 +61,10 @@
1. 将重命名后的文件发送给技术人员 1. 将重命名后的文件发送给技术人员
*📌通用版直接将生成的 mp-weixin 压缩包发送给技术人员* *📌通用版直接将生成的 mp-weixin 压缩包发送给技术人员*
2. **技术人员操作流程**: 2. **技术人员操作流程**:
- 解压压缩包 - 解压压缩包
- 确认 `site.js` 中的 `uniacid` 为客户编号 - 确认 `site.js` 中的 `uniacid` 为客户编号
- 用微信开发者工具导入解压后的代码目录 - 用微信开发者工具导入解压后的代码目录
- 编译验证无误后,上传发布 - 编译验证无误后,上传发布
##### 4.1.1.5 【本地设置】 ##### 4.1.1.5 【本地设置】
![](readme_files/1.jpg) ![](readme_files/1.jpg)
##### #####
@@ -85,66 +83,93 @@
📌 **测试版**保留原有 `/hwappx/test/` 📌 **测试版**保留原有 `/hwappx/test/`
💡 **个性化定制示例**:`/hwappx/2811/`,其中 2811 为定制化版客户编号 💡 **个性化定制示例**:`/hwappx/2811/`,其中 2811 为定制化版客户编号
3. 选择菜单栏「发行」 → 「自定义发行」 →(「H5-xcx.aigc-quickapp.com」查看.local.config.js中站点的网址前缀,站点的网址前缀是什么就选自定义发行中相同的格式) 3. 选择菜单栏「发行」 → 「自定义发行」 → 选择对应的站点脚本
例如:网址是5g.aigc-quickapp.com,自定义发行就选H5-5g.aigc-quickapp.com 💡 例如:网址是 `5g.aigc-quickapp.com` 选 `H5-5g.aigc-quickapp.com`
网址是dev.aigc-quickapp.com,自定义发行就选H5-xcx.aigc-quickapp.com
4. 修改以下配置: 4. 修改以下配置:
1. **网站标题**:快应用 1. **网站标题**:快应用
2. **网站域名**:当前客户域名(示例:xcx.aigc-quickapp.com或者5g.aigc-quickapp.com) 2. **网站域名**:当前客户域名
3. 确认后点击「发行」等待构建完成 3. 确认后点击「发行」等待构建完成
⚠️ **注意**:底部控制台弹出“项目 lucky_shop 导出Web成功,路径为:D:\项目文件\项目根目录\unpackage\dist\build\web”后再执行下一步 ⚠️ **注意**:构建完成后自动输出到 `unpackage/dist/build/h5/`,格式为 `{域名}-{base路径}.zip`
💡 **示例**:`项目 lucky_shop 导出Web成功,路径为:D:\0.项目源码\lucky_shop\unpackage\dist\build\web` 💡 **示例**:`xcx.aigc-quickapp.com-hwappx.common-3bff91e-20260511_1058.zip`
5. 按控制台提示的路径找到 `web` 目录,将该目录下所有文件手动打包成一个 `.zip` 压缩包
*仅打包文件,不包含外层 web 目录*
##### 4.2.1.2 【重命名】 ##### 4.2.1.2 【交付与最终发布】🔍
1. 按版本类型规范重命名压缩包: 1. 将生成的 `.zip` 文件发送给运维人员
- **通用版**:`hwappx-common-域名-时间.zip`
💡 **示例**:`hwappx-common-xcx.aigc-quickapp.com-2026-01-24.zip`
- **定制化版**:`客户名称-定制化---hwappx-客户编号-域名-时间.zip`
💡 **示例**:`POCT检测分析平台-定制化---hwappx-2811-xcx.aigc-quickapp.com-2026-01-24.zip`
🚫 **禁止**:压缩包命名禁止包含 `/ \ : * ? " < > |` 等特殊字符
##### 4.2.1.3 【交付与最终发布】🔍
1. 将重命名后的压缩包发送给运维人员
2. **运维人员操作流程**: 2. **运维人员操作流程**:
1. 解压压缩包 1. 解压压缩包
2. 打开快应用开发者工具,导入解压后的代码目录 2. 打开快应用开发者工具,导入解压后的代码目录
3. 验证代码无报错后,执行上传发布操作 3. 验证代码无报错后,执行上传发布操作
##### 4.2.1.3 【测试流程】
##### 4.2.1.3 【测试流程】
H5:打开自己的浏览器输入网址(https://5g.aigc-quickapp.com/hwappx/common/?uniacid=75)改站点 H5:打开自己的浏览器输入网址(https://5g.aigc-quickapp.com/hwappx/common/?uniacid=75)改站点
如果是test就把hwappx/common/改为hwappx/test/----------改站点 如果是test就把hwappx/common/改为hwappx/test/----------改站点
--- ---
## 5. 📝 构建脚本说明 ## 5. 📝 构建与版本信息
### 5.1 可用的 npm 脚本命令 ### 5.1 自动注入机制
| 命令 | 说明 | 生产构建时,`vue.config.js` 中的 `VersionInjectPlugin` 会自动执行以下操作:
| 操作 | 适用平台 | 说明 |
|------|---------|------|
| `__app_version` 全局变量注入 | 所有平台 | 构建产物的 `vendor.js`(小程序)或 `index.html`(H5)中自动内联版本数据 |
| 构建版本信息查看 | 所有平台 | 首页右下角连击 5 次弹出模态框显示 `commitId` 和 `buildTime` |
版本数据包含:
- **commitId**:当前构建的 Git 短提交 ID
- **buildTime**:构建时间
> **开发模式**:HMR 热更新时由 webpack emit 钩子自动处理,不影响开发体验。
### 5.2 自动 ZIP 打包
**H5 自定义发行**构建完成后自动执行:
| 步骤 | 说明 |
|------|------| |------|------|
| `npm run mp-weixin` | 默认构建(production模式,包含ZIP文件) | | 重命名输出目录 | `{域名}-{base路径用.连接}`(如 `xcx.aigc-quickapp.com-hwappx.common`) |
| `npm run mp-weixin:patch` | 只打补丁(production模式,不创建ZIP文件) | | 创建 ZIP 文件 | 文件名格式:`{目录名}-{Git提交ID}-{上海时区时间戳}.zip` |
| `npm run mp-weixin:dev` | 开发模式构建(development模式,包含ZIP文件) | | 清理 | ZIP 创建完成后自动删除源目录 |
| `npm run mp-weixin:dev:patch` | 只打补丁(development模式,不创建ZIP文件) |
### 5.2 构建脚本功能 💡 **示例 ZIP**:`xcx.aigc-quickapp.com-hwappx.common-3bff91e-20260511_1058.zip`
- 复制 `project.config.json` 和 `project.private.config.json` 文件到构建目录 **微信小程序**生产构建时自动执行:
- 复制 `site.js` 文件到构建目录 - 拷贝 `site.js` / `project.config.json` / `project.private.config.json` 到构建目录
- 在 `vendor.js` 文件开头追加 `import site from "../site.js";` 语句 - 创建 ZIP 包(文件名包含 Git 提交 ID)
- 支持创建构建结果的 ZIP 压缩包
- 自动打开 ZIP 文件所在目录 ### 5.3 环境配置
域名配置统一通过 `package.json` 中各脚本的 `env.UNI_APP_DOMAIN` 传入:
```json
"h5-xcx.aigc-quickapp.com": {
"title": "...",
"env": {
"UNI_PLATFORM": "h5",
"UNI_APP_DOMAIN": "https://xcx.aigc-quickapp.com/"
},
"define": { "PRODUCTION": true }
}
```
`config.js` 和 `vue.config.js` 均通过 `process.env.UNI_APP_DOMAIN` 读取,**新增/删除脚本无需改动代码**。
--- ---
## 6. 🔄 版本历史 ## 6. 🔄 版本历史
### 6.1 v1.3 ### 6.1 v1.5.1
- `VersionInjectPlugin`:webpack emit 钩子自动注入 `__app_version` 到所有平台产物
- 首页右下角连击 5 次显示构建版本信息(commitId + buildTime)
- `mp-weixin.patch.js` 精简为仅文件拷贝和 ZIP 打包
- 修复 `openWxWorkServiceChat` 方法中 fail 回调语法错误
- ZIP 文件名末尾使用 Git 提交 ID 替换时间戳
- 构建脚本幂等性修复,支持多次运行不产生重复注入
### 6.2 v1.3
- 修复微信小程序构建脚本,支持复制 `project.config.json` 和 `project.private.config.json` 文件 - 修复微信小程序构建脚本,支持复制 `project.config.json` 和 `project.private.config.json` 文件
- 增强构建脚本功能,添加命令行参数支持 - 增强构建脚本功能,添加命令行参数支持
- 在 `package.json` 中添加相关 npm 脚本命令 - 在 `package.json` 中添加相关 npm 脚本命令

View File

@@ -18,9 +18,18 @@ async function createZipWithSystemCommand(sourceDir, outputDir) {
throw new Error(`源目录不存在: ${sourceDir}`); throw new Error(`源目录不存在: ${sourceDir}`);
} }
// 生成文件名 // 获取当前 Git 提交 ID(短 hash)
let commitId = 'unknown';
try {
const { stdout } = await execAsync('git rev-parse --short HEAD', { cwd: path.join(__dirname, '..') });
commitId = stdout.trim();
} catch (e) {
console.warn('无法获取 Git 提交 ID,使用 unknown 代替');
}
// 生成文件名:mp-weixin-<日期>-<提交ID>.zip
const dateString = new Date().toISOString().split('T')[0]; const dateString = new Date().toISOString().split('T')[0];
const zipFileName = `mp-weixin-${dateString}-${new Date().getTime()}.zip`; const zipFileName = `mp-weixin-${dateString}-${commitId}.zip`;
const zipFilePath = path.join(outputDir, zipFileName); const zipFilePath = path.join(outputDir, zipFileName);
let command; let command;

View File

@@ -2,21 +2,16 @@
/** /**
* fix-wechat-miniapp.js * fix-wechat-miniapp.js
* *
* 该脚本用于修复微信小程序的一些问题: * 该脚本用于微信小程序的构建后处理(仅在生产构建时由 vue.config.js 触发):
* 1. 将项目根目录下的 site.js 文件拷贝到 unpackage/dist/build/mp-weixin 目录下,如果文件存在则覆盖 * 1. 将 project.config.json 及 project.private.config.json 拷贝到构建产物目录
* 2. 将 `const site = require("../site.js");` 这段代码追加到 * 2. 将 site.js 拷贝到构建产物目录
* unpackage/dist/build/mp-weixin/common/vendor.js 文件内容开头部分, * 3. 创建 ZIP 压缩包
* 如果这个文件开头已经有了这行代码,则不追加 *
* 注意:version.js 生成和 vendor.js 注入已由 webpack 插件 (VersionInjectPlugin) 在 emit 钩子中自动处理。
* *
* 使用: * 使用:
* node fix-wechat-miniapp.js # 打补丁并创建 ZIP 文件(默认 mode=production) * node fix-wechat-miniapp.js # 拷贝文件并创建 ZIP(默认 mode=production)
* node fix-wechat-miniapp.js --no-zip # 只打补丁,不创建 ZIP 文件 * node fix-wechat-miniapp.js --no-zip # 只拷贝文件,不创建 ZIP
* node fix-wechat-miniapp.js --mode development # 使用 development 模式打补丁
* node fix-wechat-miniapp.js --mode production # 使用 production 模式打补丁(默认)
*
* 注意:
* - 在 Windows 上路径使用反斜杠也是可以的;脚本使用 path.join 来兼容不同平台。
* - 运行前请确认在项目根目录运行(site.js 在当前工作目录下)。
*/ */
const fs = require('fs'); const fs = require('fs');
@@ -26,37 +21,19 @@ const path = require('path');
const { openFileDirectory } = require('./open-directory'); const { openFileDirectory } = require('./open-directory');
const { createZipWithSystemCommand } = require('./create-zip'); const { createZipWithSystemCommand } = require('./create-zip');
async function commonPatch(mode = 'production') { async function copyAssets(mode = 'production') {
try { try {
// 根据当前脚本所在目录(scripts),定位到项目根目录
const cwd = path.join(__dirname, '..'); const cwd = path.join(__dirname, '..');
const srcSitePath = path.join(cwd, 'site.js');
const destDir = path.join(cwd, 'unpackage', 'dist', mode === 'production' ? 'build' : 'dev', 'mp-weixin'); const destDir = path.join(cwd, 'unpackage', 'dist', mode === 'production' ? 'build' : 'dev', 'mp-weixin');
const destSitePath = path.join(destDir, 'site.js');
const vendorPath = path.join(destDir, 'common', 'vendor.js');
// 1) 检查源文件是否存在
const srcExists = await exists(srcSitePath);
if (!srcExists) {
console.error(`源文件不存在: ${srcSitePath}`);
process.exitCode = 2;
return;
}
// 确保目标目录存在
await ensureDir(destDir); await ensureDir(destDir);
// 复制 project.config.json 及 project.private.config.json 文件到 destDir 下面 // 复制 project.config.json 和 project.private.config.json
const configFiles = ['project.config.json', 'project.private.config.json']; const configFiles = ['project.config.json', 'project.private.config.json'];
for (const fileName of configFiles) { for (const fileName of configFiles) {
const srcPath = path.join(cwd, fileName); const srcPath = path.join(cwd, fileName);
const destPath = path.join(destDir, fileName); const destPath = path.join(destDir, fileName);
if (await exists(srcPath)) {
// 检查源文件是否存在
const fileExists = await exists(srcPath);
if (fileExists) {
await fsp.copyFile(srcPath, destPath); await fsp.copyFile(srcPath, destPath);
console.log(`已拷贝: ${srcPath} -> ${destPath}`); console.log(`已拷贝: ${srcPath} -> ${destPath}`);
} else { } else {
@@ -64,47 +41,15 @@ async function commonPatch(mode = 'production') {
} }
} }
// 复制 site.js 到目标目录(覆盖) // 复制 site.js
await fsp.copyFile(srcSitePath, destSitePath); const srcSitePath = path.join(cwd, 'site.js');
console.log(`已拷贝: ${srcSitePath} -> ${destSitePath}`); if (await exists(srcSitePath)) {
await fsp.copyFile(srcSitePath, path.join(destDir, 'site.js'));
// 2) 处理 vendor.js:在开头追加 require 语句(如果还没有的话) console.log(`已拷贝: ${srcSitePath} -> ${destDir}`);
const vendorExists = await exists(vendorPath); } else {
if (!vendorExists) { console.error(`源文件不存在: ${srcSitePath}`);
console.warn(`目标 vendor.js 不存在,跳过追加 require。路径: ${vendorPath}`); process.exitCode = 2;
return;
} }
let vendorContent = await fsp.readFile(vendorPath, 'utf8');
const requireLine = 'const site = require("../site");';
// 检查文件开头(例如前 5 行或前 400 字符)是否已经包含该 require 语句
const inspectPrefix = vendorContent.slice(0, 400);
const requireRegex = new RegExp(
// 匹配可能存在的引号或分号差异
'^\\s*const\\s+site\\s*=\\s*require\\s*\\([\'"]\\.\\./site[\'"]\\);?',
'm'
);
if (requireRegex.test(inspectPrefix)) {
console.log('vendor.js 开头已包含 require("../site") 语句,跳过追加。');
return;
}
// 把 requireLine + newline 插入文件最前面。
// 保留原始换行风格(若可能)
const eol = vendorContent.includes('\r\n') ? '\r\n' : '\n';
let prefix = '';
// 保留可能的 BOM
if (vendorContent.charCodeAt(0) === 0xfeff) {
prefix = '\uFEFF';
vendorContent = vendorContent.slice(1);
}
const newContent = prefix + requireLine + eol + vendorContent;
await fsp.writeFile(vendorPath, newContent, 'utf8');
console.log(`已在 vendor.js 开头追加: ${requireLine}`);
} catch (err) { } catch (err) {
console.error('发生错误:', err); console.error('发生错误:', err);
process.exitCode = 1; process.exitCode = 1;
@@ -112,33 +57,27 @@ async function commonPatch(mode = 'production') {
} }
async function main() { async function main() {
// 解析命令行参数
const argv = process.argv.slice(2); const argv = process.argv.slice(2);
const options = { const options = {
noZip: argv.includes('--no-zip'), noZip: argv.includes('--no-zip'),
mode: 'production' // 默认值 mode: 'production'
}; };
// 解析 --mode 参数
const modeIndex = argv.indexOf('--mode'); const modeIndex = argv.indexOf('--mode');
if (modeIndex !== -1 && modeIndex + 1 < argv.length) { if (modeIndex !== -1 && modeIndex + 1 < argv.length) {
options.mode = argv[modeIndex + 1]; options.mode = argv[modeIndex + 1];
} }
// 1) 打补丁 // 1) 拷贝文件
await commonPatch(options.mode); await copyAssets(options.mode);
// await commonPatch('development');
// 2) 创建 ZIP 文件(如果未指定 --no-zip) // 2) 创建 ZIP(仅 production 模式,且未指定 --no-zip)
if (!options.noZip) { if (!options.noZip && options.mode === 'production') {
const cwd = path.join(__dirname, '..'); const cwd = path.join(__dirname, '..');
const buildDir = options.mode === 'production' ? 'build' : 'dev'; const sourceDir = path.join(cwd, 'unpackage', 'dist', 'build', 'mp-weixin');
const sourceDir = path.join(cwd, 'unpackage', 'dist', buildDir, 'mp-weixin'); const destDir = path.join(cwd, 'unpackage', 'dist', 'build');
const destDir = path.join(cwd, 'unpackage', 'dist', buildDir);
const zipFilePath = await createZipWithSystemCommand(sourceDir, destDir); const zipFilePath = await createZipWithSystemCommand(sourceDir, destDir);
console.log(`ZIP 文件路径: ${zipFilePath}`); console.log(`ZIP 文件路径: ${zipFilePath}`);
// 3) 自动打开zip所在的目录
await openFileDirectory(zipFilePath); await openFileDirectory(zipFilePath);
} else { } else {
console.log('跳过创建 ZIP 文件和打开目录'); console.log('跳过创建 ZIP 文件和打开目录');
@@ -158,7 +97,6 @@ async function ensureDir(dirPath) {
try { try {
await fsp.mkdir(dirPath, { recursive: true }); await fsp.mkdir(dirPath, { recursive: true });
} catch (e) { } catch (e) {
// ignore if exists
if (e.code !== 'EEXIST') throw e; if (e.code !== 'EEXIST') throw e;
} }
} }

View File

@@ -55,10 +55,13 @@ async function openDirectoryWindows(directoryPath) {
console.log(`📁 在文件资源管理器中打开: ${normalizedPath}`); console.log(`📁 在文件资源管理器中打开: ${normalizedPath}`);
// 方法1: 使用 explorer 命令 // 方法1: 使用 explorer 命令(spawn + detached,不等待 GUI 进程)
try { try {
const command = `explorer "${normalizedPath}"`; const child = spawn('explorer', [normalizedPath], {
await execAsync(command); stdio: 'ignore',
detached: true
});
child.unref();
console.log('✅ 已启动文件资源管理器'); console.log('✅ 已启动文件资源管理器');
return true; return true;
} catch (error) { } catch (error) {

View File

@@ -1,27 +1,65 @@
const path = require('path'); const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const { zionUniMpLoadPackagePlugin } = require('zion-uniapp-mp-load-package/webpack'); const { zionUniMpLoadPackagePlugin } = require('zion-uniapp-mp-load-package/webpack');
console.log('[vue.config] 已加载');
// 确保 UNI_APP_DOMAIN 有值(从 package.json 脚本配置中读取)
if (!process.env.UNI_APP_DOMAIN && process.env.UNI_SCRIPT) {
try {
const pkg = require('./package.json');
const scriptCfg = pkg['uni-app']?.scripts?.[process.env.UNI_SCRIPT];
if (scriptCfg?.env?.UNI_APP_DOMAIN) {
process.env.UNI_APP_DOMAIN = scriptCfg.env.UNI_APP_DOMAIN;
}
} catch (e) {}
}
// 输出 H5 域名信息到控制台
if (process.env.UNI_PLATFORM === 'h5') {
console.log(`\n[H5 发行] 脚本: ${process.env.UNI_SCRIPT || '(默认)'}`);
console.log(`[H5 发行] 域名: ${process.env.UNI_APP_DOMAIN || '(未配置)'}\n`);
}
// 打印出process.env // 打印出process.env
// for (let key in process.env) { // for (let key in process.env) {
// console.log(key, process.env[key]); // console.log(key, process.env[key]);
// } // }
// 定义UNI_OUTPUT_DIR环境变量 // 定义UNI_OUTPUT_DIR环境变量(H5 自定义发行脚本使用)
try { try {
if (!process.env.NODE_ENV) { if (!process.env.NODE_ENV) {
throw new Error('NODE_ENV 环境变量未设置'); throw new Error('NODE_ENV 环境变量未设置');
} }
const buildDir = process.env.NODE_ENV === 'production' ? 'build' : 'dev'; const buildDir = process.env.NODE_ENV === 'production' ? 'build' : 'dev';
const platform = process.env.UNI_PLATFORM || '';
const UNI_SCRIPT = process.env.UNI_SCRIPT ?? ''; const UNI_SCRIPT = process.env.UNI_SCRIPT ?? '';
if ([ // 所有 H5 自定义脚本统一处理,无需在 vue.config.js 中硬编码脚本名
// 参照 package.json 中定义的 "uni-app" --> "scripts" if (platform === 'h5' && UNI_SCRIPT) {
'h5-xcx20.5g-quickapp.com', 'h5-xcx.aigc-quickapp.com', 'h5-5g.aigc-quickapp.com', 'h5-test.aigc-quickapp.com' // 从 manifest.json 读取 h5.router.base,拼接到输出路径
].includes(UNI_SCRIPT)) { let baseSuffix = '';
process.env.UNI_OUTPUT_DIR = path.join(__dirname, `unpackage/dist/${buildDir}/h5/${UNI_SCRIPT}`); try {
const manifestPath = path.join(__dirname, 'manifest.json');
baseSuffix = _readManifestBase(manifestPath);
} catch (e) {}
// 从脚本 env 中获取域名(优先使用 UNI_APP_DOMAIN,否则从 UNI_SCRIPT 提取)
let domain = '';
if (process.env.UNI_APP_DOMAIN) {
try {
domain = new URL(process.env.UNI_APP_DOMAIN).hostname;
} catch (e) {
domain = process.env.UNI_APP_DOMAIN.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
}
} else {
domain = UNI_SCRIPT.replace(/^h5-/, '');
}
// 格式: {域名}-{base用.连接}
const outputDirName = baseSuffix ? `${domain}-${baseSuffix}` : domain;
process.env.UNI_OUTPUT_DIR = path.join(__dirname, `unpackage/dist/${buildDir}/h5/${outputDirName}`);
console.log('UNI_OUTPUT_DIR = ', process.env.UNI_OUTPUT_DIR); console.log('UNI_OUTPUT_DIR = ', process.env.UNI_OUTPUT_DIR);
} }
@@ -29,6 +67,211 @@ try {
console.log('NODE_ENV 环境变量未设置,默认使用开发模式'); console.log('NODE_ENV 环境变量未设置,默认使用开发模式');
} }
/**
* 读取 manifest.json 中的 h5.router.base,去掉注释后解析
* 返回 base 路径的 . 连接形式(如 /hwappx/common/ → hwappx.common)
*/
function _readManifestBase(manifestPath) {
try {
const raw = fs.readFileSync(manifestPath, 'utf8');
// 去掉 /* */ 和 // 注释(HBuilder 生成的 JSON 允许注释)
const clean = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
const manifest = JSON.parse(clean);
if (manifest?.h5?.router?.base) {
return manifest.h5.router.base.replace(/^\/+|\/+$/g, '').replace(/\//g, '.');
}
} catch (e) {
console.warn('[manifest] 读取 h5.router.base 失败:', e.message);
}
return '';
}
/**
* 自定义 webpack 插件:在通用产物(vendor.js)中内联注入版本数据
* 利用 emit 钩子在资源写入磁盘前处理,支持 HMR(每次重编译都会自动注入)
* 适用于所有平台(mp-weixin / h5 等)
*/
class VersionInjectPlugin {
constructor() {
this._cachedCommitId = null;
}
apply(compiler) {
compiler.hooks.emit.tapAsync('VersionInjectPlugin', (compilation, callback) => {
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';
const platform = process.env.UNI_PLATFORM || '';
// 获取 Git 提交 ID(缓存,避免每次 HMR 都执行)
if (!this._cachedCommitId) {
try {
this._cachedCommitId = execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim();
} catch (e) {
this._cachedCommitId = 'unknown';
}
}
const versionData = { commitId: this._cachedCommitId, buildTime: new Date().toISOString() };
const inlineVersion = JSON.stringify(versionData);
// 尝试注入到 vendor.js(mp-weixin / mp-* 等平台)
const vendorKey = 'common/vendor.js';
const vendorAsset = compilation.assets[vendorKey];
if (vendorAsset) {
let content = typeof vendorAsset.source === 'function' ? vendorAsset.source() : vendorAsset._value;
if (typeof content !== 'string') content = content.toString();
// 清理旧注入
content = content.replace(/^\s*const\s+site\s*=\s*require\s*\(['"]\.\.\/site(\.js)?['"]\);?\s*\r?\n/gm, '');
content = content.replace(/^\s*const\s+__app_version\s*=\s*(require\s*\(['"]\.\.\/version(\.json|\.js)?['"]\)|.+);?\s*\r?\n/gm, '');
// 只注入版本信息(通用),site.js require 仅在 mp-weixin 生产构建时注入
const lines = [`const __app_version = ${inlineVersion};`];
if (platform === 'mp-weixin' && mode === 'production') {
lines.unshift('const site = require("../site.js");');
}
const eol = content.includes('\r\n') ? '\r\n' : '\n';
let prefix = '';
if (content.charCodeAt(0) === 0xfeff) {
prefix = '\uFEFF';
content = content.slice(1);
}
content = prefix + lines.join(eol) + eol + content;
compilation.assets[vendorKey] = {
source: () => content,
size: () => content.length
};
console.log(`[VersionInject] ✅ vendor.js 已注入 (platform=${platform}, mode=${mode}, commit=${this._cachedCommitId})`);
} else {
// H5 等平台可能没有 common/vendor.js,改为尝试注入到 index.html
// #ifdef H5
const htmlKey = 'index.html';
const htmlAsset = compilation.assets[htmlKey];
if (htmlAsset) {
let html = typeof htmlAsset.source === 'function' ? htmlAsset.source() : htmlAsset._value;
if (typeof html !== 'string') html = html.toString();
// 在 </head> 前注入 script 标签
const scriptTag = `<script>var __app_version = ${inlineVersion};</script>`;
if (!html.includes('__app_version')) {
html = html.replace('</head>', `${scriptTag}\n</head>`);
compilation.assets[htmlKey] = {
source: () => html,
size: () => html.length
};
console.log(`[VersionInject] ✅ index.html 已注入 (platform=${platform}, commit=${this._cachedCommitId})`);
}
}
// #endif
}
callback();
});
// 生产构建后处理:mp-weixin 拷贝 site.js + ZIP;H5 自动打包 ZIP
if (process.env.NODE_ENV === 'production') {
compiler.hooks.afterEmit.tapAsync('ProductionZipPack', (compilation, callback) => {
const platform = process.env.UNI_PLATFORM || '';
const UNI_SCRIPT = process.env.UNI_SCRIPT ?? '';
if (platform === 'mp-weixin') {
console.log('\n🚀 mp-weixin 生产构建完成,运行补丁脚本(site.js 拷贝 + ZIP 打包)...');
try {
execSync(`"${process.execPath}" scripts/mp-weixin.patch.js --mode production`, { cwd: __dirname, stdio: 'inherit' });
} catch (e) {
console.error('补丁脚本执行失败:', e.message);
}
} else if (platform === 'h5' && UNI_SCRIPT) {
this._zipH5Build(compilation);
}
callback();
});
}
}
_computeH5OutputName() {
// 计算输出目录名: {域名}-{base用.连接}
let baseSuffix = _readManifestBase(path.join(__dirname, 'manifest.json'));
let domain = '';
if (process.env.UNI_APP_DOMAIN) {
try {
domain = new URL(process.env.UNI_APP_DOMAIN).hostname;
} catch (e) {
domain = process.env.UNI_APP_DOMAIN.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
}
} else {
domain = (process.env.UNI_SCRIPT || '').replace(/^h5-/, '');
}
const result = baseSuffix ? `${domain}-${baseSuffix}` : domain;
console.log('[H5Zip] 输出目录名:', result);
return result;
}
_zipH5Build(compilation) {
const outputDirName = this._computeH5OutputName();
const buildDir = process.env.NODE_ENV === 'production' ? 'build' : 'dev';
const h5Dir = path.join(__dirname, 'unpackage', 'dist', buildDir, 'h5');
// 实际构建输出目录(由 webpack 决定)
const actualOutput = compilation.compiler.outputPath;
const targetDir = path.join(h5Dir, outputDirName);
// 如果路径一致,跳过重命名
if (actualOutput !== targetDir) {
try {
if (fs.existsSync(targetDir)) {
fs.rmSync(targetDir, { recursive: true });
}
fs.renameSync(actualOutput, targetDir);
console.log(`[H5Zip] 输出目录已重命名为: ${outputDirName}`);
} catch (e) {
console.error('[H5Zip] 目录重命名失败:', e.message);
return;
}
} else {
console.log(`[H5Zip] 输出目录名正确: ${outputDirName}`);
}
// 标准时区时间戳格式: 20260511_1058
const fmt = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', hour12: false
});
const parts = fmt.formatToParts(new Date());
const get = (t) => parts.find(p => p.type === t).value;
const ts = `${get('year')}${get('month')}${get('day')}_${get('hour')}${get('minute')}`;
const zipFileName = `${outputDirName}-${this._cachedCommitId}-${ts}.zip`;
const zipFilePath = path.join(h5Dir, zipFileName);
console.log(`\n📦 H5 ZIP: ${zipFileName}...`);
let command;
const sourcePath = targetDir.replace(/'/g, "''");
const destPath = zipFilePath.replace(/'/g, "''");
if (process.platform === 'win32') {
command = `powershell -Command "Compress-Archive -Path '${sourcePath}\\*' -DestinationPath '${destPath}' -Force"`;
} else {
const sourceBase = path.dirname(targetDir);
const sourceName = path.basename(targetDir);
command = `cd '${sourceBase}' && zip -r '${zipFilePath}' '${sourceName}' -x '*/node_modules/*' '*.git/*' '*.DS_Store'`;
}
try {
execSync(command, { stdio: 'inherit' });
console.log(`✅ H5 ZIP: ${zipFileName}`);
// 删除已打包的源目录
// fs.rmSync(targetDir, { recursive: true, force: true });
// console.log(`[H5Zip] 已删除源目录: ${outputDirName}`);
} catch (e) {
console.error('H5 ZIP 打包失败:', e.message);
}
}
}
// 导出配置 // 导出配置
module.exports = { module.exports = {
productionSourceMap: false, productionSourceMap: false,
@@ -118,5 +361,8 @@ module.exports = {
} }
} }
} }
// 所有模式下添加版本注入插件(适用于所有平台)
config.plugins.push(new VersionInjectPlugin());
} }
}; };