Compare commits
54 Commits
557addb76b
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
| 8901e302e4 | |||
| 68bcd3ae7c | |||
| 9f89172829 | |||
| ddd1ec3056 | |||
| d305268cb8 | |||
| 1efb866e41 | |||
| b8182a0f97 | |||
| 37463a173c | |||
| 422ca8cdb5 | |||
| f92b1fb5b7 | |||
| a0043418c8 | |||
| 927f763d0a | |||
| 69ad814985 | |||
| 92599453c2 | |||
| 5d7facc94d | |||
| f6eb1fad11 | |||
| 83b253bced | |||
| 19cb332645 | |||
| e5406a27ba | |||
| ffa170ff13 | |||
| 3b81ecdf98 | |||
| c536fac4d2 | |||
| 475449f04e | |||
| e40b46a888 | |||
| 8a95b0310d | |||
| a81386d336 | |||
| b69eeba74b | |||
| f656be789d | |||
| 82139e616d | |||
| ed6d9f563d | |||
| 3a8ad7d871 | |||
| ec960a29c4 | |||
| 8312de5d09 | |||
| 5833a244de | |||
| 92fe95d131 | |||
| 07718235c3 | |||
| 183181a15d | |||
| a4f2c81528 | |||
| 30b66ad7b3 | |||
| 077fac40c2 | |||
| 10d45b1c3b | |||
| 60dc054383 | |||
| ef8c7568ac | |||
| 551d6c103d | |||
| 4a92d9bec2 | |||
| ff4340c3a9 | |||
| 4303a93278 | |||
| 12e4493a17 | |||
| c94e214d05 | |||
| afede79047 | |||
| ab727eef1d | |||
| ee050f5df9 | |||
| 855fbb2fe5 | |||
| b706e67e82 |
15
.gitignore
vendored
15
.gitignore
vendored
@@ -61,6 +61,21 @@ 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
|
||||
|
||||
111
.local.config.js
111
.local.config.js
@@ -21,7 +21,7 @@ const localDevConfig = ({
|
||||
'2881': { // 防爆电器
|
||||
uniacid: 2881,
|
||||
domain: 'https://xcx6.aigc-quickapp.com/',
|
||||
},
|
||||
},
|
||||
'2724': { // 生物菌肥
|
||||
uniacid: 2724,
|
||||
domain: 'https://xcx.aigc-quickapp.com/',
|
||||
@@ -45,11 +45,11 @@ const localDevConfig = ({
|
||||
'11-test': { // 测试平台
|
||||
uniacid: 11,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
},
|
||||
'18-test': { // 测试平台
|
||||
uniacid: 18,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
uniacid: 18,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
'local-2': { // 测试平台
|
||||
uniacid: 2,
|
||||
domain: 'http://localhost:8050/',
|
||||
@@ -59,15 +59,15 @@ 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/',
|
||||
},
|
||||
'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/',
|
||||
@@ -83,15 +83,15 @@ const localDevConfig = ({
|
||||
'5g-11': {
|
||||
uniacid: 11,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
},
|
||||
'5g-206': {
|
||||
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/',
|
||||
@@ -99,27 +99,27 @@ const localDevConfig = ({
|
||||
'145': { // 表单链接转义问题
|
||||
uniacid: 145,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
},
|
||||
'5g-213': { // 单图组点击链接无法跳转
|
||||
uniacid: 213,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
},
|
||||
'5g-4': { // 公告点击链接无法跳转
|
||||
uniacid: 4,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
},
|
||||
'5g-97': { // 大宗商品贸易,支持自定义字体大小、颜色
|
||||
uniacid: 97,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
},
|
||||
'5g-50': { // 燃气安全产品机器人,购物车无反应
|
||||
uniacid: 50,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
},
|
||||
'5g-237': { // 商品分类,没有立即咨询文字
|
||||
uniacid: 237,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
},
|
||||
'2847': { //
|
||||
uniacid: 2847,
|
||||
domain: 'https://xcx.aigc-quickapp.com/',
|
||||
@@ -132,30 +132,59 @@ const localDevConfig = ({
|
||||
uniacid: 2485,
|
||||
domain: 'https://xcx6.aigc-quickapp.com/',
|
||||
},
|
||||
'2821': { // 离型纸硅油纸
|
||||
uniacid: 2821,
|
||||
domain: 'https://xcx.aigc-quickapp.com/',
|
||||
},
|
||||
'18': { // 测试平台
|
||||
uniacid: 18,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
'2': { // 测试平台
|
||||
uniacid: 2,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
'48': { // 测试平台
|
||||
uniacid: 48,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
'2821': { // 离型纸硅油纸
|
||||
uniacid: 2821,
|
||||
domain: 'https://xcx.aigc-quickapp.com/',
|
||||
},
|
||||
'18': { // 测试平台
|
||||
uniacid: 18,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
'2': { // 测试平台
|
||||
uniacid: 2,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
'48': { // 测试平台
|
||||
uniacid: 48,
|
||||
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/',
|
||||
},
|
||||
},
|
||||
'248': { // 大宗商品贸易,支持自定义字体大小、颜色
|
||||
uniacid: 248,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
'228': { // 商品分类,没有立即咨询文字
|
||||
uniacid: 228,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
'2797': { //气雾栽培机,视频组件安卓ios播放正常
|
||||
uniacid: 2797,
|
||||
domain: 'https://xcx.aigc-quickapp.com/',
|
||||
},
|
||||
'5g-95': { // 商品分类,没有立即咨询文字
|
||||
uniacid: 95,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
'5g-160': { // 过瘤胃瘤胃保护,商品详情视频
|
||||
uniacid: 160,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
'5g-268': { // 合成孔径雷达,一级分类背景字体颜色
|
||||
uniacid: 268,
|
||||
domain: 'https://5g.aigc-quickapp.com/',
|
||||
},
|
||||
'225-test': { // 测试平台
|
||||
uniacid: 225,
|
||||
domain: 'https://test.aigc-quickapp.com',
|
||||
},
|
||||
'192.168.8.118-321': { // 测试平台
|
||||
uniacid: 321,
|
||||
domain: 'http://192.168.8.118:8050/',
|
||||
// domain: 'http://localhost:8050/',
|
||||
},
|
||||
// })['5g-237']; // 商品分类,没有立即咨询文字
|
||||
// })['5g-97']; // 大宗商品贸易,支持自定义字体大小、颜色
|
||||
// })['5g-50']; // 燃气安全产品机器人,购物车无反应
|
||||
|
||||
@@ -118,6 +118,8 @@
|
||||
.goods-video video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.goods-video .uni-video-cover {
|
||||
|
||||
@@ -4,8 +4,56 @@ import store from '@/store/index.js'
|
||||
import lang from './lang.js'
|
||||
|
||||
let currentConversationId = null;
|
||||
|
||||
// ====== AI Agent Generalization (customer service is the first agent) ======
|
||||
// setAgentId updates currentAgentId; default is agent_aikefu.
|
||||
// Any sendMessage / sendStreamMessage / getServiceConfig call passes agent_id automatically.
|
||||
//
|
||||
// PR-7: 按 agent_id 路由 endpoint
|
||||
// - agent_aikefu (默认智能客服) -> 走 aikefu addon (/api/kefu/* + /ws/aikefu)
|
||||
// - 其他 agent (knowledge / quick_reply / sales / hr) -> 走 ai-agent addon (/api/aiagent/* + /ws/aiagent)
|
||||
// - 前端不关心后端两个 addon 之间的差异,只关心 agent_id 路由
|
||||
|
||||
export const DEFAULT_AGENT_ID = 'agent_aikefu';
|
||||
let currentAgentId = DEFAULT_AGENT_ID;
|
||||
export const CONVERSATION_KEY = 'ai_conversation_id';
|
||||
|
||||
/**
|
||||
* PR-7: agent_id -> 后端 addon 路由映射
|
||||
*
|
||||
* 规则:
|
||||
* - agent_aikefu (默认智能客服) -> aikefu addon (老路径 /api/kefu/*)
|
||||
* - 其他 agent -> aiagent addon (新路径 /api/aiagent/*)
|
||||
*
|
||||
* 后端两个 addon 完全独立:
|
||||
* - aikefu: lucky_aikefu_config / conversation / message
|
||||
* - aiagent: lucky_ai_agent_config / conversation / message
|
||||
*/
|
||||
function resolveAddonName(agentId) {
|
||||
if (!agentId || agentId === 'agent_aikefu') return 'aikefu';
|
||||
return 'aiagent';
|
||||
}
|
||||
|
||||
/**
|
||||
* PR-7: agent_id -> API endpoint URL
|
||||
* agent_aikefu -> '/api/kefu/...' (兼容老路径)
|
||||
* 其他 -> '/api/aiagent/...' (ai-agent 新路径)
|
||||
*/
|
||||
function resolveApiUrl(apiName) {
|
||||
var addon = resolveAddonName(currentAgentId);
|
||||
if (addon === 'aikefu') {
|
||||
return '/api/kefu/' + apiName;
|
||||
}
|
||||
return '/api/aiagent/' + apiName;
|
||||
}
|
||||
|
||||
/**
|
||||
* PR-7: WebSocket URL 路径(/ws/{addonName})
|
||||
*/
|
||||
function resolveWsPath() {
|
||||
return '/ws/' + resolveAddonName(currentAgentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取域名配置参数(避免重复代码)
|
||||
*/
|
||||
@@ -35,12 +83,12 @@ const GUEST_ID_KEY = 'ai_guest_user_id';
|
||||
function getUserId() {
|
||||
const memberId = store.state.memberInfo?.id;
|
||||
if (memberId) return memberId;
|
||||
|
||||
|
||||
let guestId;
|
||||
try {
|
||||
guestId = uni.getStorageSync(GUEST_ID_KEY);
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
|
||||
if (!guestId) {
|
||||
guestId = 'guest_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10);
|
||||
try {
|
||||
@@ -50,6 +98,20 @@ function getUserId() {
|
||||
return guestId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 AI Agent。
|
||||
*/
|
||||
function getAgentId() {
|
||||
return currentAgentId || DEFAULT_AGENT_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前 AI Agent。
|
||||
*/
|
||||
function setAgentId(agentId) {
|
||||
if (agentId && typeof agentId === 'string') currentAgentId = agentId;
|
||||
}
|
||||
|
||||
// #ifdef MP
|
||||
// ================================
|
||||
// WebSocket 长连接管理(微信小程序)
|
||||
@@ -91,7 +153,9 @@ function ensureWebSocketConnected() {
|
||||
return;
|
||||
}
|
||||
|
||||
var wssUrl = 'wss://' + domain + '/ws/aikefu';
|
||||
// PR-7: WebSocket 路径按当前 agent_id 路由 — agent_aikefu 走 /ws/aikefu,其他走 /ws/aiagent
|
||||
var wsPath = resolveWsPath();
|
||||
var wssUrl = 'wss://' + domain + wsPath;
|
||||
wsConnected = false;
|
||||
wsAuthenticated = false;
|
||||
wsSocketTask = wx.connectSocket({ url: wssUrl, header: {} });
|
||||
@@ -106,6 +170,7 @@ function ensureWebSocketConnected() {
|
||||
wsSocketTask.send({
|
||||
data: JSON.stringify({
|
||||
action: 'auth',
|
||||
agent_id: getAgentId(),
|
||||
uniacid: _uniacid,
|
||||
token: _token,
|
||||
user_id: _userId
|
||||
@@ -233,19 +298,35 @@ export default {
|
||||
return uni.getStorageSync('uniacid') || Config.uniacid; // 从缓存中获取uniacid,或使用默认uniacid
|
||||
},
|
||||
|
||||
/**
|
||||
* PR-7: 获取当前 agent 对应的 addon 名称 (aikefu / aiagent)
|
||||
*/
|
||||
getAddonName() {
|
||||
return resolveAddonName(currentAgentId);
|
||||
},
|
||||
|
||||
/**
|
||||
* PR-7: 获取当前 agent 对应的 API endpoint 前缀
|
||||
*/
|
||||
getApiPrefix() {
|
||||
return resolveAddonName(currentAgentId) === 'aikefu' ? '/api/kefu' : '/api/aiagent';
|
||||
},
|
||||
|
||||
/**
|
||||
* 发送普通消息(blocking 模式)
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async sendMessage(message, options = {}) {
|
||||
try {
|
||||
const params = {
|
||||
url: '/api/kefu/chat',
|
||||
url: resolveApiUrl('chat'),
|
||||
data: {
|
||||
agent_id: getAgentId(),
|
||||
user_id: getUserId(),
|
||||
stream: false,
|
||||
inputs: {},
|
||||
query: message,
|
||||
response_mode: 'blocking', // 强制 blocking
|
||||
response_mode: 'blocking',
|
||||
user: getUserId(),
|
||||
conversation_id: this.getConversationId() || '',
|
||||
...getDomainParams()
|
||||
@@ -268,7 +349,6 @@ export default {
|
||||
} catch (error) {
|
||||
console.error('[AI Service] AiChat API请求失败,详细错误:', error);
|
||||
console.error('[AI Service] 错误堆栈:', error.stack);
|
||||
// 打印更多信息帮助调试
|
||||
console.error('[AI Service] store 状态:', {
|
||||
memberInfo: store.state.memberInfo,
|
||||
uniacid: this.getUniacid(),
|
||||
@@ -284,7 +364,6 @@ export default {
|
||||
*/
|
||||
async sendStreamMessage(message, onChunk, onComplete) {
|
||||
// #ifdef MP
|
||||
// 先确保连接已就绪
|
||||
try {
|
||||
await ensureWebSocketConnected();
|
||||
} catch (e) {
|
||||
@@ -292,7 +371,6 @@ export default {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 设置当前消息回调
|
||||
wsMessageContent = '';
|
||||
wsMessageActive = true;
|
||||
wsMessageOnChunk = onChunk || null;
|
||||
@@ -305,6 +383,7 @@ export default {
|
||||
wsSocketTask.send({
|
||||
data: JSON.stringify({
|
||||
action: 'chat',
|
||||
agent_id: getAgentId(),
|
||||
uniacid: this.getUniacid(),
|
||||
query: message,
|
||||
user_id: getUserId(),
|
||||
@@ -315,13 +394,16 @@ export default {
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* HTTP 流式请求(仅 H5 使用)
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async sendHttpStream(message, onChunk, onComplete) {
|
||||
// #ifdef H5
|
||||
try {
|
||||
const url = Config.baseUrl + `/api/kefu/chat`;
|
||||
var chatUrl = resolveApiUrl('chat');
|
||||
var url = Config.baseUrl + chatUrl;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -334,6 +416,7 @@ export default {
|
||||
inputs: {},
|
||||
query: message,
|
||||
response_mode: 'streaming',
|
||||
agent_id: getAgentId(),
|
||||
user_id: getUserId(),
|
||||
conversation_id: this.getConversationId() || '',
|
||||
...getDomainParams()
|
||||
@@ -356,9 +439,8 @@ export default {
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// 按行分割,保留不完整的最后一行
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // 未完成的行留到下次
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
@@ -369,14 +451,12 @@ export default {
|
||||
const data = JSON.parse(jsonStr);
|
||||
if (data.event === 'message') {
|
||||
var text = data.answer || data.text || '';
|
||||
// 兼容嵌套 data.data 格式
|
||||
if (!text && data.data) {
|
||||
text = data.data.content || data.data.answer || '';
|
||||
}
|
||||
content += text;
|
||||
if (onChunk) onChunk(text);
|
||||
}
|
||||
// 兼容两种层级:conversation_id 可能在顶层或 data.data 中
|
||||
var cid = data.conversation_id || (data.data && data.data.conversation_id) || '';
|
||||
if (cid) {
|
||||
conversationId = cid;
|
||||
@@ -392,7 +472,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最后残留的 buffer(如果有)
|
||||
if (buffer.trim().startsWith('data:')) {
|
||||
try {
|
||||
const jsonStr = buffer.trim().slice(5);
|
||||
@@ -479,11 +558,13 @@ export default {
|
||||
|
||||
/**
|
||||
* 生成新会话(如需)
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async generateConversationId() {
|
||||
const params = {
|
||||
url: '/api/kefu/createConversation',
|
||||
url: resolveApiUrl('createConversation'),
|
||||
data: {
|
||||
agent_id: getAgentId(),
|
||||
uniacid: this.getUniacid(),
|
||||
token: store.state.token || 'test_token',
|
||||
user_id: getUserId(),
|
||||
@@ -503,11 +584,12 @@ export default {
|
||||
|
||||
/**
|
||||
* 获取服务状态
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async getServiceStatus() {
|
||||
try {
|
||||
const response = await http.sendRequest({
|
||||
url: '/api/kefu/health',
|
||||
url: resolveApiUrl('health'),
|
||||
async: false,
|
||||
data: {}
|
||||
});
|
||||
@@ -527,12 +609,14 @@ export default {
|
||||
|
||||
/**
|
||||
* 获取服务配置信息
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async getServiceConfig() {
|
||||
try {
|
||||
const params = {
|
||||
url: '/api/kefu/info',
|
||||
url: resolveApiUrl('info'),
|
||||
data: {
|
||||
agent_id: getAgentId(),
|
||||
uniacid: this.getUniacid(),
|
||||
token: store.state.token || 'test_token',
|
||||
user_id: getUserId(),
|
||||
@@ -542,7 +626,7 @@ export default {
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
};
|
||||
const response = await http.sendRequest({
|
||||
...params,
|
||||
async: false,
|
||||
@@ -557,12 +641,14 @@ export default {
|
||||
|
||||
/**
|
||||
* 清除会话历史(调用后端接口)
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async clearConversation(conversationId) {
|
||||
try {
|
||||
const response = await http.sendRequest({
|
||||
url: '/api/kefu/clear-conversation',
|
||||
url: resolveApiUrl('clear-conversation'),
|
||||
data: {
|
||||
agent_id: getAgentId(),
|
||||
conversation_id: conversationId,
|
||||
...getDomainParams()
|
||||
},
|
||||
@@ -577,6 +663,7 @@ export default {
|
||||
|
||||
/**
|
||||
* 获取会话历史
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
async getConversationHistory(params = {}) {
|
||||
try {
|
||||
@@ -585,6 +672,7 @@ export default {
|
||||
}
|
||||
console.log('[AI History] 请求历史, conversation_id:', params.conversation_id, 'limit:', params.limit);
|
||||
const requestData = {
|
||||
agent_id: params.agent_id || getAgentId(),
|
||||
uniacid: params.uniacid || this.getUniacid(),
|
||||
conversation_id: params.conversation_id,
|
||||
user_id: params.user_id || getUserId(),
|
||||
@@ -595,7 +683,7 @@ export default {
|
||||
...getDomainParams()
|
||||
};
|
||||
const response = await http.sendRequest({
|
||||
url: '/api/kefu/getHistory',
|
||||
url: resolveApiUrl('getHistory'),
|
||||
data: requestData,
|
||||
header: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -625,6 +713,7 @@ export default {
|
||||
|
||||
/**
|
||||
* [H5 Only] EventSource 流式聊天(备用)
|
||||
* PR-7: URL 按 agent_id 路由
|
||||
*/
|
||||
chatWithEventSource(message, conversationId = '', onMessage, onComplete, onError) {
|
||||
// #ifdef H5
|
||||
@@ -632,6 +721,7 @@ export default {
|
||||
window.currentEventSource.close();
|
||||
}
|
||||
const paramsObj = {
|
||||
agent_id: getAgentId(),
|
||||
uniacid: this.getUniacid(),
|
||||
user_id: getUserId(),
|
||||
member_id: getUserId(),
|
||||
@@ -642,7 +732,8 @@ export default {
|
||||
...getDomainParams()
|
||||
};
|
||||
const params = new URLSearchParams(paramsObj);
|
||||
const url = `${Config.baseUrl}/api/kefu/chat?${params.toString()}`;
|
||||
var chatUrl = resolveApiUrl('chat');
|
||||
const url = `${Config.baseUrl}${chatUrl}?${params.toString()}`;
|
||||
try {
|
||||
const eventSource = new EventSource(url);
|
||||
window.currentEventSource = eventSource;
|
||||
@@ -687,7 +778,6 @@ export default {
|
||||
*/
|
||||
async chatWithFetchStream(message, conversationId = '', onMessage, onComplete, onError) {
|
||||
// #ifdef H5
|
||||
// 实际逻辑已在 sendHttpStream 中实现,此处可复用或留空
|
||||
return this.sendHttpStream(message, onMessage, (res) => {
|
||||
onComplete?.({ message: res.content, conversation_id: res.conversation_id });
|
||||
});
|
||||
@@ -711,4 +801,26 @@ export default {
|
||||
// H5 无 WebSocket 长连接,无需操作
|
||||
},
|
||||
// #endif
|
||||
};
|
||||
|
||||
/**
|
||||
* Set current AI Agent (default aikefu; link system passes agent_id).
|
||||
* PR-7: 切换 agent 时, 如果跨 addon 则断开 WebSocket 重连 (路径变了)
|
||||
* @param {string} agentId
|
||||
*/
|
||||
setAgentId(agentId) {
|
||||
var oldAddon = resolveAddonName(currentAgentId);
|
||||
setAgentId(agentId);
|
||||
var newAddon = resolveAddonName(currentAgentId);
|
||||
if (oldAddon !== newAddon) {
|
||||
try { disconnectWebSocket(); } catch (e) { /* ignore */ }
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current AI Agent.
|
||||
* @returns {string}
|
||||
*/
|
||||
getAgentId() {
|
||||
return getAgentId();
|
||||
},
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import IMAGE_CONFIG from './image-config.js';
|
||||
import Platform from './platform.js';
|
||||
import PAGE_KEY_MAP from './util.js';
|
||||
|
||||
export class CustomerService {
|
||||
constructor(vueInstance, externalConfig = null) {
|
||||
@@ -118,6 +119,16 @@ export class CustomerService {
|
||||
|
||||
/**
|
||||
* 跳转到 AI智能客服页面(AiChat)
|
||||
*
|
||||
* 注意:不在此处注入 __dyu=FloatBtn。
|
||||
* 该标记原本是 diy-float-btn.vue 用来告诉 ai-chat "我是从浮动按钮来的,请隐藏底部 tabBar"。
|
||||
* 此方法是 customer-service.js 的**通用 AI 客服入口**,被多处复用:
|
||||
* - hover-nav(悬浮客服按钮)
|
||||
* - 商品详情页 / 会员中心 / 个人中心 等的"联系客服"按钮
|
||||
* - DIY 浮动按钮(diy-float-btn 内部已自行注入 __dyu=FloatBtn,这里再注入会重复)
|
||||
* 若在此处统一注入,会让所有 AI 客服入口都隐藏底部 tabBar,语义错误且侵入性过强。
|
||||
* hover-nav 需要隐藏 tabBar 的场景,由 hover-nav.vue#handleUnifiedKefuClick
|
||||
* 自行注入 __dyu=FloatBtn(与 diy-float-btn.vue#onFloatBtnClick 一致)。
|
||||
*/
|
||||
openAiChatService() {
|
||||
try {
|
||||
@@ -127,7 +138,7 @@ export class CustomerService {
|
||||
}
|
||||
|
||||
// ✅ 修正路径:必须与 pages.json 中注册的路径一致
|
||||
const aiChatUrl = '/pages_tool/ai-chat/index';
|
||||
const aiChatUrl = PAGE_KEY_MAP.AI_CHAT_PAGE_URL;
|
||||
|
||||
// ✅ 使用 navigateTo 保留返回栈(体验更好)
|
||||
uni.navigateTo({
|
||||
@@ -551,6 +562,9 @@ export class CustomerService {
|
||||
|
||||
/**
|
||||
* 打开 AI 智能助手
|
||||
*
|
||||
* 注意:不在此处注入 __dyu=FloatBtn,理由同 openAiChatService()。
|
||||
* 由调用方(hover-nav.vue 等)按需注入。
|
||||
*/
|
||||
openAIChat() {
|
||||
try {
|
||||
@@ -558,7 +572,7 @@ export class CustomerService {
|
||||
this.vm.setAiUnreadCount(0);
|
||||
}
|
||||
|
||||
const aiChatUrl = '/pages_tool/ai-chat/index';
|
||||
const aiChatUrl = PAGE_KEY_MAP.AI_CHAT_PAGE_URL;
|
||||
|
||||
uni.navigateTo({
|
||||
url: aiChatUrl,
|
||||
|
||||
@@ -71,6 +71,10 @@ export default {
|
||||
...{
|
||||
diy_category_tree_select_size: '28rpx',// 左侧分类树选中态字号
|
||||
diy_category_tree_normal_size: '20rpx',// 左侧分类树未选中态字号
|
||||
diy_category_tree_select_text_color: '#333',// 左侧分类树选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_select_bg_color: '#fff',// 左侧分类树选中态背景颜色(后台可自定义)
|
||||
diy_category_tree_normal_text_color: '#222222',// 左侧分类树未选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_normal_bg_color: '#f5f5f5',// 左侧分类树未选中态背景颜色(后台可自定义)
|
||||
diy_category_item_title_size: '26rpx',// 分类标题(如"热销推荐")
|
||||
diy_category_item_goods_title_size:'28rpx',// 商品模式分类标题(比普通标题大 2rpx)
|
||||
diy_category_item_name_size: '24rpx',// 分类项名称
|
||||
@@ -264,6 +268,10 @@ export default {
|
||||
...{
|
||||
diy_category_tree_select_size: '28rpx',// 左侧分类树选中态字号
|
||||
diy_category_tree_normal_size: '20rpx',// 左侧分类树未选中态字号
|
||||
diy_category_tree_select_text_color: '#333',// 左侧分类树选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_select_bg_color: '#fff',// 左侧分类树选中态背景颜色(后台可自定义)
|
||||
diy_category_tree_normal_text_color: '#222222',// 左侧分类树未选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_normal_bg_color: '#f5f5f5',// 左侧分类树未选中态背景颜色(后台可自定义)
|
||||
diy_category_item_title_size: '26rpx',// 分类标题(如"热销推荐")
|
||||
diy_category_item_goods_title_size:'28rpx',// 商品模式分类标题(比普通标题大 2rpx)
|
||||
diy_category_item_name_size: '24rpx',// 分类项名称
|
||||
@@ -456,6 +464,10 @@ export default {
|
||||
...{
|
||||
diy_category_tree_select_size: '28rpx',// 左侧分类树选中态字号
|
||||
diy_category_tree_normal_size: '20rpx',// 左侧分类树未选中态字号
|
||||
diy_category_tree_select_text_color: '#333',// 左侧分类树选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_select_bg_color: '#fff',// 左侧分类树选中态背景颜色(后台可自定义)
|
||||
diy_category_tree_normal_text_color: '#222222',// 左侧分类树未选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_normal_bg_color: '#f5f5f5',// 左侧分类树未选中态背景颜色(后台可自定义)
|
||||
diy_category_item_title_size: '26rpx',// 分类标题(如"热销推荐")
|
||||
diy_category_item_goods_title_size:'28rpx',// 商品模式分类标题(比普通标题大 2rpx)
|
||||
diy_category_item_name_size: '24rpx',// 分类项名称
|
||||
@@ -648,6 +660,10 @@ export default {
|
||||
...{
|
||||
diy_category_tree_select_size: '28rpx',// 左侧分类树选中态字号
|
||||
diy_category_tree_normal_size: '20rpx',// 左侧分类树未选中态字号
|
||||
diy_category_tree_select_text_color: '#333',// 左侧分类树选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_select_bg_color: '#fff',// 左侧分类树选中态背景颜色(后台可自定义)
|
||||
diy_category_tree_normal_text_color: '#222222',// 左侧分类树未选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_normal_bg_color: '#f5f5f5',// 左侧分类树未选中态背景颜色(后台可自定义)
|
||||
diy_category_item_title_size: '26rpx',// 分类标题(如"热销推荐")
|
||||
diy_category_item_goods_title_size:'28rpx',// 商品模式分类标题(比普通标题大 2rpx)
|
||||
diy_category_item_name_size: '24rpx',// 分类项名称
|
||||
@@ -840,6 +856,10 @@ export default {
|
||||
...{
|
||||
diy_category_tree_select_size: '28rpx',// 左侧分类树选中态字号
|
||||
diy_category_tree_normal_size: '20rpx',// 左侧分类树未选中态字号
|
||||
diy_category_tree_select_text_color: '#333',// 左侧分类树选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_select_bg_color: '#fff',// 左侧分类树选中态背景颜色(后台可自定义)
|
||||
diy_category_tree_normal_text_color: '#222222',// 左侧分类树未选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_normal_bg_color: '#f5f5f5',// 左侧分类树未选中态背景颜色(后台可自定义)
|
||||
diy_category_item_title_size: '26rpx',// 分类标题(如"热销推荐")
|
||||
diy_category_item_goods_title_size:'28rpx',// 商品模式分类标题(比普通标题大 2rpx)
|
||||
diy_category_item_name_size: '24rpx',// 分类项名称
|
||||
@@ -1032,6 +1052,10 @@ export default {
|
||||
...{
|
||||
diy_category_tree_select_size: '28rpx',// 左侧分类树选中态字号
|
||||
diy_category_tree_normal_size: '20rpx',// 左侧分类树未选中态字号
|
||||
diy_category_tree_select_text_color: '#333',// 左侧分类树选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_select_bg_color: '#fff',// 左侧分类树选中态背景颜色(后台可自定义)
|
||||
diy_category_tree_normal_text_color: '#222222',// 左侧分类树未选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_normal_bg_color: '#f5f5f5',// 左侧分类树未选中态背景颜色(后台可自定义)
|
||||
diy_category_item_title_size: '26rpx',// 分类标题(如"热销推荐")
|
||||
diy_category_item_goods_title_size:'28rpx',// 商品模式分类标题(比普通标题大 2rpx)
|
||||
diy_category_item_name_size: '24rpx',// 分类项名称
|
||||
@@ -1224,6 +1248,10 @@ export default {
|
||||
...{
|
||||
diy_category_tree_select_size: '28rpx',// 左侧分类树选中态字号
|
||||
diy_category_tree_normal_size: '20rpx',// 左侧分类树未选中态字号
|
||||
diy_category_tree_select_text_color: '#333',// 左侧分类树选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_select_bg_color: '#fff',// 左侧分类树选中态背景颜色(后台可自定义)
|
||||
diy_category_tree_normal_text_color: '#222222',// 左侧分类树未选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_normal_bg_color: '#f5f5f5',// 左侧分类树未选中态背景颜色(后台可自定义)
|
||||
diy_category_item_title_size: '26rpx',// 分类标题(如"热销推荐")
|
||||
diy_category_item_goods_title_size:'28rpx',// 商品模式分类标题(比普通标题大 2rpx)
|
||||
diy_category_item_name_size: '24rpx',// 分类项名称
|
||||
@@ -1416,6 +1444,10 @@ export default {
|
||||
...{
|
||||
diy_category_tree_select_size: '28rpx',// 左侧分类树选中态字号
|
||||
diy_category_tree_normal_size: '20rpx',// 左侧分类树未选中态字号
|
||||
diy_category_tree_select_text_color: '#333',// 左侧分类树选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_select_bg_color: '#fff',// 左侧分类树选中态背景颜色(后台可自定义)
|
||||
diy_category_tree_normal_text_color: '#222222',// 左侧分类树未选中态字体颜色(后台可自定义)
|
||||
diy_category_tree_normal_bg_color: '#f5f5f5',// 左侧分类树未选中态背景颜色(后台可自定义)
|
||||
diy_category_item_title_size: '26rpx',// 分类标题(如"热销推荐")
|
||||
diy_category_item_goods_title_size:'28rpx',// 商品模式分类标题(比普通标题大 2rpx)
|
||||
diy_category_item_name_size: '24rpx',// 分类项名称
|
||||
|
||||
@@ -139,6 +139,7 @@ export const CONTACT_PAGE_URL = '/pages_tool/contact/contact';
|
||||
export const MEMBER_PAGE_URL = '/pages_tool/member/index';
|
||||
export const LOGIN_PAGE_URL = '/pages_tool/login/login';
|
||||
export const AI_CHAT_PAGE_URL = '/pages_tool/ai-chat/index';
|
||||
export const AI_AGENT_PAGE_URL = '/pages_tool/ai-agent/index'; // AI 智能体入口页(智能客服是首个 agent)
|
||||
|
||||
// page_key 到实际路径的映射表(统一管理所有页面的语义常量 → 实际路径)
|
||||
// 前端组织页面(重命名路径、分包调整)时只需要修改此映射表
|
||||
@@ -1203,6 +1204,23 @@ export default {
|
||||
* @param {Object} link
|
||||
*/
|
||||
diyRedirectTo(link) {
|
||||
|
||||
// ---------- AI Agent 通用链接(智能客服是首个 agent;新增 AI 销售/HR 等不用改这段) ----------
|
||||
// 放在 stale-uniacid 与 resolveLinkUrl 之前:AI Agent 是纯内部路径,走后续 webview 分支会被错判。
|
||||
if (link && (link.name === 'AI_AGENT' || link.kind === 'AI_AGENT')) {
|
||||
const agentId = (link.page_params && link.page_params.agent_id) || 'agent_aikefu';
|
||||
const title = link.title || 'AI 智能体';
|
||||
// 透传来源标记:浮动按钮点击时 __dyu=FloatBtn,目标页据此隐藏底部 tabBar
|
||||
// (diy-float-btn.vue#onFloatBtnClick 已写入 page_params.__dyu)
|
||||
const dyuEntry = (link.page_params && link.page_params.__dyu) || '';
|
||||
return this.redirectTo(AI_AGENT_PAGE_URL, {
|
||||
agent_id: agentId,
|
||||
title: encodeURIComponent(title),
|
||||
enable_set_title: link.enable_set_title ? 1 : 0,
|
||||
...(dyuEntry ? { __dyu: dyuEntry } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`-----> diyRedirectTo`, JSON.stringify(link));
|
||||
//if (link == null || Object.keys(link).length == 1) return;
|
||||
|
||||
|
||||
2090
common/js/util.js.bak
Normal file
2090
common/js/util.js.bak
Normal file
File diff suppressed because it is too large
Load Diff
92
common/js/video-helper.js
Normal file
92
common/js/video-helper.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// iOS 微信小程序 <video> 兼容工具
|
||||
// 问题背景:
|
||||
// 1) 弹窗内的 <video> 用 autoplay 不可靠——popup 初始隐藏,autoplay 在隐藏态不触发,
|
||||
// 等 popup 显示后也不会重新 autoplay,导致"安卓有时能播、iOS 不能";
|
||||
// 2) iOS 对同一个 <video> 实例在 src 重绑 / 弹窗重开 / 播放结束后,不会自动重新加载,
|
||||
// 需要靠 :key 重建实例或 seek(0) 才能重播;
|
||||
// 3) iOS 微信不支持 webm / vp9 / hevc / ogv 等封装或编码,会直接黑屏;
|
||||
// 4) H5 端(含微信内置浏览器)$nextTick 调 play() 太快,弹窗动画未完时浏览器内部
|
||||
// 会自动 pause(),导致 "The play() request was interrupted by a call to pause()" 错误。
|
||||
// 这里统一提供"格式检测"与"在视频可见后触发播放"的能力,供各视频组件复用。
|
||||
//
|
||||
// 重要:微信小程序的 <video>.play() 不要求用户手势(与 H5 不同),只要在视频元素
|
||||
// 已经渲染、popup 已经显示后调用即可。因此请务必在 uni-popup 的 @change(show=true)
|
||||
// 回调里调用本方法,而不是在 open() 里紧跟着 $nextTick 调用(那时元素尚未可见)。
|
||||
|
||||
// iOS 微信 <video> 已知不支持的封装/编码(仅按扩展名做保守判断,用于给出友好提示)
|
||||
const UNSUPPORTED_EXT_ON_IOS = ['webm', 'ogv', 'ogg', 'mov', 'm4v', 'hevc', 'av1', '3gp'];
|
||||
|
||||
// 取 URL 中的小写扩展名(忽略查询参数与 hash)
|
||||
export function getVideoExt(url) {
|
||||
if (!url || typeof url !== 'string') return '';
|
||||
const clean = url.split('?')[0].split('#')[0];
|
||||
const idx = clean.lastIndexOf('.');
|
||||
if (idx < 0) return '';
|
||||
return clean.slice(idx + 1).toLowerCase();
|
||||
}
|
||||
|
||||
// 该地址在 iOS 上是否很可能无法播放(基于扩展名的保守判断)
|
||||
export function isLikelyUnsupportedOnIOS(url) {
|
||||
const ext = getVideoExt(url);
|
||||
if (!ext) return false;
|
||||
return UNSUPPORTED_EXT_ON_IOS.indexOf(ext) !== -1;
|
||||
}
|
||||
|
||||
// 在"视频已经渲染、popup 已经显示"的前提下触发播放。
|
||||
// 必须在 uni-popup 的 @change(show=true) 回调里调用,此时 <video> 元素已可见,
|
||||
// createVideoContext 才能拿到有效实例。
|
||||
// videoId: <video> 的 id;instance: 当前组件/页面实例(组件内必传)。
|
||||
export function playVideoInContext(videoId, instance) {
|
||||
if (!videoId || !instance) return;
|
||||
|
||||
// H5 端需要更长延迟:等 uni-popup 动画播完(~350ms),否则浏览器内部会自动
|
||||
// pause() 导致 "play() interrupted by pause()" 错误;小程序端 $nextTick 即可。
|
||||
// #ifdef H5
|
||||
const delay = 400;
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
const delay = 0;
|
||||
// #endif
|
||||
|
||||
const doPlay = () => {
|
||||
let ctx = null;
|
||||
try {
|
||||
ctx = uni.createVideoContext(videoId, instance);
|
||||
} catch (e) {
|
||||
console.error('[video-helper] createVideoContext failed:', e);
|
||||
return;
|
||||
}
|
||||
if (!ctx) return;
|
||||
|
||||
try {
|
||||
ctx.play();
|
||||
} catch (e) {
|
||||
console.error('[video-helper] play() failed, will retry:', e);
|
||||
// H5 端首次 play() 常被浏览器自动 pause() 打断,延迟重试一次
|
||||
// #ifdef H5
|
||||
setTimeout(() => {
|
||||
try { ctx.play(); } catch (retryErr) {
|
||||
console.error('[video-helper] retry play() also failed:', retryErr);
|
||||
}
|
||||
}, 300);
|
||||
// #endif
|
||||
}
|
||||
};
|
||||
|
||||
if (delay > 0) {
|
||||
// H5:先等 nextTick(DOM 更新),再等 delay(弹窗动画完成)
|
||||
if (typeof instance.$nextTick === 'function') {
|
||||
instance.$nextTick(() => setTimeout(doPlay, delay));
|
||||
} else {
|
||||
setTimeout(doPlay, delay);
|
||||
}
|
||||
} else {
|
||||
// 小程序端:nextTick 即可
|
||||
if (typeof instance.$nextTick === 'function') {
|
||||
instance.$nextTick(doPlay);
|
||||
} else {
|
||||
doPlay();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,9 @@
|
||||
</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>
|
||||
@@ -110,6 +113,9 @@
|
||||
</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>
|
||||
@@ -173,6 +179,9 @@
|
||||
</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>
|
||||
@@ -1420,6 +1429,26 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,10 +784,10 @@ export default {
|
||||
padding: 26rpx 28rpx;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
background-color: #f5f5f5;
|
||||
background-color: var(--diy-category-tree-normal-bg-color, #f5f5f5);
|
||||
|
||||
view {
|
||||
color: #222222;
|
||||
color: var(--diy-category-tree-normal-text-color, #222222);
|
||||
width: 100%;
|
||||
white-space: break-word;
|
||||
overflow: hidden;
|
||||
@@ -804,10 +804,10 @@ export default {
|
||||
}
|
||||
|
||||
&.select {
|
||||
background: #fff;
|
||||
background: var(--diy-category-tree-select-bg-color, #fff);
|
||||
|
||||
view {
|
||||
color: #333;
|
||||
color: var(--diy-category-tree-select-text-color, #333);
|
||||
font-weight: bold;
|
||||
font-size: var(--diy-category-tree-select-size, 28rpx);
|
||||
}
|
||||
@@ -1512,11 +1512,11 @@ export default {
|
||||
// 左侧分类栏样式
|
||||
.tree-wrap .category-item {
|
||||
padding: 28rpx 16rpx;
|
||||
background-color: #fff;
|
||||
background-color: var(--diy-category-tree-normal-bg-color, #f5f5f5);
|
||||
border-radius: 0;
|
||||
|
||||
view {
|
||||
color: #666;
|
||||
color: var(--diy-category-tree-normal-text-color, #222222);
|
||||
font-size: 26rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
@@ -1527,10 +1527,10 @@ export default {
|
||||
}
|
||||
|
||||
&.select {
|
||||
background: #fff;
|
||||
background: var(--diy-category-tree-select-bg-color, #fff);
|
||||
|
||||
view {
|
||||
color: var(--base-color, #FF6B35);
|
||||
color: var(--diy-category-tree-select-text-color, #333);
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
@@ -1603,22 +1603,22 @@ export default {
|
||||
|
||||
.tree-wrap .category-item {
|
||||
padding: 30rpx 16rpx;
|
||||
background-color: #f5f5f5;
|
||||
background-color: var(--diy-category-tree-normal-bg-color, #f5f5f5);
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
|
||||
view {
|
||||
color: #333;
|
||||
color: var(--diy-category-tree-normal-text-color, #222222);
|
||||
font-size: 26rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&.select {
|
||||
background: #fff;
|
||||
background: var(--diy-category-tree-select-bg-color, #fff);
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
view {
|
||||
color: #333;
|
||||
color: var(--diy-category-tree-select-text-color, #333);
|
||||
font-weight: bold;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
:class="{ left_top: value.bottomPosition == 1, right_top: value.bottomPosition == 2, left_bottom: value.bottomPosition == 3, right_bottom: value.bottomPosition == 4 }"
|
||||
:style="style">
|
||||
<block v-for="(item, index) in value.list" :key="index">
|
||||
<view class="button-box" @tap.stop="$util.diyRedirectTo(item.link)"
|
||||
<view class="button-box" @tap.stop="onFloatBtnClick(item)"
|
||||
:style="{ width: value.imageSize + 'px', height: value.imageSize + 'px', fontSize: value.imageSize + 'px' }">
|
||||
<image v-if="!item.iconType || item.iconType == 'img'" :src="$util.img(item.imageUrl)" mode="aspectFit"
|
||||
show-menu-by-longpress />
|
||||
@@ -44,7 +44,44 @@ export default {
|
||||
componentRefresh: function (nval) { }
|
||||
},
|
||||
components: {},
|
||||
methods: {},
|
||||
methods: {
|
||||
/**
|
||||
* 浮动按钮点击处理:
|
||||
* 在跳转前给 link 注入 __dyu=FloatBtn 来源标记,
|
||||
* 让目标页面(尤其是 ai-chat)能识别"来自浮动按钮"从而隐藏底部 tabBar,
|
||||
* 与"从底部导航栏点击进入时显示并高亮 tabBar"的场景并存。
|
||||
*
|
||||
* 为什么用 __dyu 这种 3 字符命名:
|
||||
* - 双下划线 __ 前缀 + 3 位字母(dy-ui 框架) 是前端约定俗成的"系统内部保留字段"标志
|
||||
* (如 __proto__, __esModule),业务方配置 link 时几乎不可能使用这种短命名 → 与业务参数零冲突
|
||||
* - URL 中占用空间极小,不污染业务 query 字符串
|
||||
* - 未来如需扩展其他组件的入口来源,可继续使用 __dyu 前缀(如 __dyu=Banner / __dyu=Nav)
|
||||
*
|
||||
* 实现要点:
|
||||
* - 浅拷贝 item.link 避免污染原数据(可能被 v-for 复用)
|
||||
* - 把标记写入 page_params,diyRedirectTo → redirectTo 会自动拼到 URL query,
|
||||
* 目标页 onLoad(options) 可直接读取
|
||||
* - 同时在 wap_url 末尾追加/合并 __dyu=FloatBtn 兜底:某些路径不走 page_params 拼接
|
||||
*/
|
||||
onFloatBtnClick(item) {
|
||||
if (!item || !item.link) return;
|
||||
const link = { ...item.link };
|
||||
link.page_params = Object.assign({}, link.page_params || {}, {
|
||||
__dyu: 'FloatBtn'
|
||||
});
|
||||
|
||||
// 兜底:直接把标记拼到 wap_url query,防止 page_params 在某些分支未被透传
|
||||
if (link.wap_url && typeof link.wap_url === 'string' && link.wap_url.indexOf('http') === -1) {
|
||||
const sep = link.wap_url.indexOf('?') === -1 ? '?' : '&';
|
||||
// 避免重复追加(精确匹配 __dyu= 而不是 __dyu,避免误伤可能的 __dyu_other)
|
||||
if (link.wap_url.indexOf('__dyu=') === -1) {
|
||||
link.wap_url = link.wap_url + sep + '__dyu=FloatBtn';
|
||||
}
|
||||
}
|
||||
|
||||
this.$util.diyRedirectTo(link);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
let style = {},
|
||||
|
||||
@@ -209,6 +209,10 @@ export default {
|
||||
// 占位符:遗漏会被 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'),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,17 +54,21 @@
|
||||
<!-- 视频播放弹窗 -->
|
||||
<uni-popup ref="videoPopup" type="center" style="background:transparent;width:100%;height:100%;">
|
||||
<view class="video-container" :style="popupVideoStyle">
|
||||
<video
|
||||
<video
|
||||
:key="video_url"
|
||||
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>
|
||||
@@ -144,41 +148,21 @@ export default {
|
||||
|
||||
// 弹窗视频容器样式
|
||||
popupVideoStyle() {
|
||||
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
|
||||
|
||||
// 容器横向铺满,黑色背景填满左右边界
|
||||
// 视频用 object-fit:contain 保持原比例居中,左右黑边由容器黑色背景填充
|
||||
let style = 'position:fixed;width:100%;height:50vh;left:50%;top:50%;transform:translate(-50%,-50%);border-radius:0;';
|
||||
return style;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 显示视频播放弹窗
|
||||
showVideo(item) {
|
||||
this.video_url = item.videoUrl;
|
||||
let url = item.videoUrl || '';
|
||||
// 处理视频URL:完整链接直接使用,否则通过 $util.img() 转换
|
||||
if (url && url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
|
||||
url = this.$util.img(url);
|
||||
}
|
||||
this.video_url = url;
|
||||
this.popupVideoWidth = 0;
|
||||
this.popupVideoHeight = 0;
|
||||
this.$refs.videoPopup.open();
|
||||
@@ -625,22 +609,6 @@ 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,6 +11,9 @@
|
||||
:style="videoWarpCss"
|
||||
controls
|
||||
preload="metadata"
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
x5-playsinline
|
||||
@loadedmetadata="onVideoLoaded"
|
||||
@click="handlerClick(videoUrl)"
|
||||
></video>
|
||||
@@ -106,11 +109,11 @@ export default {
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
style += 'padding-top: ' + (ratio * 100) + '%;';
|
||||
style += 'height:0;padding-top: ' + (ratio * 100) + '%;';
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
style += 'padding-top: ' + (ratio * 100) + '%;';
|
||||
style += 'height:0;padding-top: ' + (ratio * 100) + '%;';
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-HARMONY
|
||||
|
||||
@@ -743,7 +743,53 @@ export default {
|
||||
this.$langConfig.change(targetLang, url);
|
||||
},
|
||||
|
||||
/**
|
||||
* 客服按钮点击的统一入口。
|
||||
*
|
||||
* 关键修复:智能客服(AI 客服)类型需要走 ai-chat 页面,
|
||||
* 且必须在 URL 上注入 __dyu=FloatBtn 标记,
|
||||
* 让 ai-chat/index.vue 的 onLoad(options) 识别"来自悬浮按钮/浮动按钮",
|
||||
* commit setBottomNavHidden(true) 隐藏底部 tabBar。
|
||||
*
|
||||
* 为什么不在 customer-service.js#openAiChatService 统一注入:
|
||||
* 该方法是通用 AI 客服入口,被多处复用(hover-nav / 商品详情页 / 会员中心 / DIY 浮动按钮等),
|
||||
* 其中 DIY 浮动按钮(diy-float-btn.vue)已自行注入 __dyu=FloatBtn;其他入口(商品详情页等)
|
||||
* 是否要隐藏 tabBar 是另一个独立的产品决策,不能在此一刀切。
|
||||
* 因此由调用方按需注入:本方法(hover-nav)显式注入,diy-float-btn 显式注入,
|
||||
* 其他入口保持原样。
|
||||
*
|
||||
* 与 diy-float-btn.vue#onFloatBtnClick 的行为完全一致,只是触发位置不同。
|
||||
*/
|
||||
handleUnifiedKefuClick() {
|
||||
// 仅智能客服(aikefu)类型需要走 ai-chat 页面并隐藏 tabBar;
|
||||
// 其他类型(企业微信 / 第三方 / 小程序原生客服等)由 customerService 内部
|
||||
// 路由到对应渠道(navigateToMiniProgram / openCustomerServiceChat 等),
|
||||
// 与底部 tabBar 无关,继续走原统一入口。
|
||||
if (this.customerService && this.customerService.isAIKefu && this.customerService.isAIKefu()) {
|
||||
// 清除未读数(与 openAiChatService 行为保持一致)
|
||||
if (typeof this.setAiUnreadCount === 'function') {
|
||||
this.setAiUnreadCount(0);
|
||||
} else if (this.$store && typeof this.$store.commit === 'function') {
|
||||
try { this.$store.commit('setAiUnreadCount', 0); } catch (e) { /* 兼容旧 store */ }
|
||||
}
|
||||
|
||||
// 注入 __dyu=FloatBtn 标记,与 diy-float-btn.vue#onFloatBtnClick 语义一致
|
||||
const aiChatUrl = `${this.$util.PAGE_KEY_MAP.PAGE_AI_CHAT}?__dyu=FloatBtn`;
|
||||
uni.navigateTo({
|
||||
url: aiChatUrl,
|
||||
fail: (err) => {
|
||||
console.error('[hover-nav] 跳转 AI智能客服失败:', err);
|
||||
// H5 兜底
|
||||
// #ifdef H5
|
||||
window.location.href = aiChatUrl;
|
||||
// #endif
|
||||
uni.showToast({ title: this.$lang('customerService.openFailed'), icon: 'none' });
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 非智能客服类型:走原统一入口(企业微信 / 第三方 / 微信原生客服 等)
|
||||
this.customerService.handleUnifiedKefuClick({
|
||||
sendMessageTitle: '来自悬浮按钮的咨询',
|
||||
sendMessagePath: '/pages/index/index'
|
||||
|
||||
@@ -26,38 +26,17 @@
|
||||
<text class="required" :style="{ color: requiredColor }">{{ item.value.required ? '*' : '' }}</text>
|
||||
</view>
|
||||
<view class="box">
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<!-- 鸿蒙元服务渲染为原生 ArkUI TextArea,必须用 :rows 控制初始显示行数;CSS height 在原生组件上不生效 -->
|
||||
<!-- 所有平台统一通过 formConfig.textareaRows / item.value.rows 控制显示行数:
|
||||
- H5 / 微信:原生 textarea 识别 :rows 属性,可视高度 = rows × line-height(CSS 控制 line-height);
|
||||
- 鸿蒙:渲染为原生 ArkUI TextArea,CSS height 不生效,必须靠 :rows 控制。
|
||||
行数和行高全部由 formConfig 动态下发,不再硬编码 176rpx / row=2。 -->
|
||||
<textarea
|
||||
:rows="item.value.rows || 2"
|
||||
:rows="item.value.rows || textareaRowsValue"
|
||||
:placeholder="item.value.placeholder"
|
||||
placeholder-class="placeholder color-tip"
|
||||
v-model="item.val"
|
||||
:style="inputStyle"
|
||||
:style="textareaStyle"
|
||||
></textarea>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-HARMONY -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- 微信小程序原生 textarea:
|
||||
不用 auto-height(会随内容无限扩展遮挡后续组件),改用 textareaStyle 中
|
||||
固定 height 176rpx(约 4 行)+ line-height 44rpx,可视区域比原 88rpx 翻倍,
|
||||
内容多时内部滚动条 thumb 比例改善一倍,且高度可控。 -->
|
||||
<textarea
|
||||
:placeholder="item.value.placeholder"
|
||||
placeholder-class="placeholder color-tip"
|
||||
v-model="item.val"
|
||||
:style="textareaStyle"
|
||||
></textarea>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-WEIXIN -->
|
||||
<textarea
|
||||
:placeholder="item.value.placeholder"
|
||||
placeholder-class="placeholder color-tip"
|
||||
v-model="item.val"
|
||||
:style="inputStyle"
|
||||
></textarea>
|
||||
<!-- #endif -->
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -301,6 +280,11 @@
|
||||
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',
|
||||
@@ -334,6 +318,8 @@
|
||||
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'),
|
||||
@@ -394,6 +380,8 @@
|
||||
'--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,
|
||||
@@ -434,21 +422,37 @@
|
||||
'line-height': this.$util.rpx(this.config.inputHeight)
|
||||
});
|
||||
},
|
||||
// textarea 专用 inline style(仅微信小程序端使用,见 template 中 #ifdef MP-WEIXIN 分支)
|
||||
// textarea 显示行数(来自 formConfig.textareaRows,未配置时回退到默认 4)
|
||||
textareaRowsValue() {
|
||||
const n = parseInt(this.config.textareaRows, 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : 4;
|
||||
},
|
||||
// textarea 行高(来自 formConfig.textareaLineHeight,用于 :rows × line-height 算出固定可视高度)
|
||||
textareaLineHeightValue() {
|
||||
const n = parseInt(this.config.textareaLineHeight, 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : 44;
|
||||
},
|
||||
// textarea 专用 inline style(所有平台统一使用,见 template 中 textarea 节点)
|
||||
// 设计要点:
|
||||
// 1) 不复用 inputStyle 的 min-height / line-height:这两个值取自 inputHeight(默认 60rpx),
|
||||
// 对 textarea 太小,会让微信小程序原生 textarea 单行过高、可视区域过小,
|
||||
// 内容稍多即溢出导致原生滚动条 thumb 异常小;
|
||||
// 2) 不使用 auto-height:在微信小程序原生 textarea 上,auto-height 会让 textarea 随内容
|
||||
// 无限扩展,遮挡后续组件,导致"看不到下面的其他组件";
|
||||
// 3) 改用固定 height 176rpx(约 4 行,line-height 44rpx),相比原 CSS height: 88rpx(2 行)
|
||||
// 可视区域翻倍,内容多时内部滚动条 thumb 比例改善一倍,且高度可控不会遮挡后续组件。
|
||||
// 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 h = this.$util.rpx(176);
|
||||
const lh = this.$util.rpx(this.textareaLineHeightValue);
|
||||
const rows = this.textareaRowsValue;
|
||||
// 加上 padding(12rpx 上下) 和 border(1rpx 上下) 的占用,避免与 :rows 算出的高度不一致时被截断
|
||||
const paddings = this.$util.rpx(24) + this.$util.rpx(2);
|
||||
const h = (this.$util.rpx(this.textareaLineHeightValue * (rows - 1)) + lh) + paddings;
|
||||
return this.$util.objToStyle({
|
||||
'height': h,
|
||||
'min-height': h,
|
||||
'line-height': this.$util.rpx(44),
|
||||
'line-height': lh,
|
||||
'border-radius': this.$util.rpx(this.config.inputRadius),
|
||||
'font-size': this.$util.rpx(this.config.labelFontSize),
|
||||
'color': this.config.labelColor
|
||||
@@ -959,6 +963,9 @@
|
||||
--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;
|
||||
@@ -1094,11 +1101,12 @@
|
||||
font-size: var(--label-font-size, $font-size-base);
|
||||
width: 100%;
|
||||
color: var(--label-color, #1f2937);
|
||||
// 鸿蒙元服务渲染为原生 ArkUI TextArea,不识别 CSS height,由 :rows 控制行数;
|
||||
// 这里给所有端都设一个最小高度基线,避免空内容时塌缩
|
||||
min-height: 88rpx;
|
||||
height: 88rpx;
|
||||
line-height: 44rpx;
|
||||
// 行高由 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);
|
||||
text-align: left;
|
||||
border-radius: var(--input-radius, 0rpx);
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -27,10 +27,16 @@
|
||||
<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"
|
||||
v-model="item.val"
|
||||
:style="textareaStyle"
|
||||
></textarea>
|
||||
</view>
|
||||
</view>
|
||||
@@ -377,6 +383,10 @@ export default {
|
||||
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',
|
||||
@@ -452,6 +462,9 @@ 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,
|
||||
@@ -497,6 +510,30 @@ 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),
|
||||
@@ -997,6 +1034,9 @@ 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;
|
||||
@@ -1234,8 +1274,10 @@ export default {
|
||||
::v-deep textarea {
|
||||
font-size: var(--label-font-size, 28rpx);
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
line-height: 1.5;
|
||||
// 行高 / 高度全部由 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);
|
||||
@@ -1244,7 +1286,6 @@ export default {
|
||||
color: var(--label-color, #1f2937);
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
|
||||
&:focus {
|
||||
background: var(--input-focus-bg-color, #ffffff);
|
||||
@@ -1260,8 +1301,10 @@ export default {
|
||||
textarea {
|
||||
font-size: var(--label-font-size, 28rpx);
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
line-height: 1.5;
|
||||
// 行高 / 高度全部由 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);
|
||||
text-align: left;
|
||||
border: 1rpx solid var(--input-border-color, #e5e7eb);
|
||||
border-radius: var(--input-radius, 10rpx);
|
||||
@@ -1270,7 +1313,6 @@ export default {
|
||||
color: var(--label-color, #1f2937);
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
|
||||
&:focus {
|
||||
background: var(--input-focus-bg-color, #ffffff);
|
||||
|
||||
@@ -9,9 +9,13 @@
|
||||
<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>
|
||||
@@ -22,6 +26,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { playVideoInContext } from '@/common/js/video-helper.js';
|
||||
|
||||
export default {
|
||||
name: 'ns-video-player-popup',
|
||||
props: {
|
||||
@@ -76,6 +82,10 @@ export default {
|
||||
* @param {Object} e - 弹窗状态变化事件
|
||||
*/
|
||||
onVideoPopupChange(e) {
|
||||
// H5 端弹窗显示后触发播放:避免 autoplay 被浏览器拦截 / 弹窗动画未完成被 pause() 打断
|
||||
if (e && e.show) {
|
||||
playVideoInContext('popupVideoPlayer', this);
|
||||
}
|
||||
this.$emit('popup-change', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,4 +136,8 @@ export const lang = {
|
||||
logInitNavbarFailed: 'Failed to initialize navigation bar',
|
||||
logChatComponentNotInit: 'Chat component not initialized',
|
||||
logServicerConfigNotLoaded: '[AI Customer Service] servicerConfig not loaded, retrying after 200ms...'
|
||||
}
|
||||
|
||||
// ====== AI Agent general copy (customer service is the first agent) ======
|
||||
aiAgentDefaultTitle: 'AI Agent',
|
||||
aiAgentOpenFailed: 'Failed to open AI Agent, please try again later',
|
||||
aiAgentNotConfigured: 'This AI Agent is not enabled'}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'AI智能客服',
|
||||
title: 'AI智能',
|
||||
|
||||
// ====== 通用 ======
|
||||
cancel: '取消',
|
||||
@@ -136,4 +136,8 @@ export const lang = {
|
||||
logInitNavbarFailed: '初始化导航栏失败',
|
||||
logChatComponentNotInit: '聊天组件未初始化',
|
||||
logServicerConfigNotLoaded: '【AI客服】servicerConfig 未加载,200ms后重试...'
|
||||
}
|
||||
|
||||
// ====== AI Agent 通用文案(智能客服是首个 agent) ======
|
||||
aiAgentDefaultTitle: 'AI 智能体',
|
||||
aiAgentOpenFailed: '打开 AI 智能体失败,请稍后再试',
|
||||
aiAgentNotConfigured: '该 AI 智能体暂未启用'}
|
||||
|
||||
61
pages.json
61
pages.json
@@ -1086,7 +1086,64 @@
|
||||
{
|
||||
"path": "ai-chat/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "AI智能客服"
|
||||
"navigationBarTitleText": "AI智能客服",
|
||||
"componentPlaceholder": {
|
||||
"diy-article": "view",
|
||||
"diy-audio": "view",
|
||||
"diy-bargain": "view",
|
||||
"diy-bottom-nav": "view",
|
||||
"diy-category": "view",
|
||||
"diy-category-item": "view",
|
||||
"diy-channel-list": "view",
|
||||
"diy-channel-video": "view",
|
||||
"diy-comp-extend": "view",
|
||||
"diy-coupon": "view",
|
||||
"diy-digit": "view",
|
||||
"diy-fenxiao-goods-list": "view",
|
||||
"diy-float-btn": "view",
|
||||
"diy-follow-official-account": "view",
|
||||
"diy-form": "view",
|
||||
"diy-goods-brand": "view",
|
||||
"diy-goods-list": "view",
|
||||
"diy-goods-recommend": "view",
|
||||
"diy-graphic-nav": "view",
|
||||
"diy-group": "view",
|
||||
"diy-groupbuy": "view",
|
||||
"diy-horz-blank": "view",
|
||||
"diy-horz-line": "view",
|
||||
"diy-hot-area": "view",
|
||||
"diy-icon": "view",
|
||||
"diy-image-nav": "view",
|
||||
"diy-img-ads": "view",
|
||||
"diy-index-page": "view",
|
||||
"diy-kefu": "view",
|
||||
"diy-listmenu": "view",
|
||||
"diy-live": "view",
|
||||
"diy-many-goods-list": "view",
|
||||
"diy-map": "view",
|
||||
"diy-member-info": "view",
|
||||
"diy-member-my-order": "view",
|
||||
"diy-merch-list": "view",
|
||||
"diy-notes": "view",
|
||||
"diy-notice": "view",
|
||||
"diy-payment-qrcode": "view",
|
||||
"diy-picture": "view",
|
||||
"diy-pinfan": "view",
|
||||
"diy-pintuan": "view",
|
||||
"diy-presale": "view",
|
||||
"diy-quick-nav": "view",
|
||||
"diy-recommend-code": "view",
|
||||
"diy-rich-text": "view",
|
||||
"diy-rubik-cube": "view",
|
||||
"diy-search": "view",
|
||||
"diy-seckill": "view",
|
||||
"diy-store": "view",
|
||||
"diy-store-label": "view",
|
||||
"diy-tab": "view",
|
||||
"diy-text": "view",
|
||||
"diy-video": "view",
|
||||
"diy-video-list": "view"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2154,7 +2211,7 @@
|
||||
"style": {
|
||||
// APP 端原本就是 custom;微信小程序 / 华为快应用 / 鸿蒙元服务
|
||||
// 的原生 navbar 不支持自定义左侧 Home 按钮,需要切到 custom 自绘
|
||||
// #ifdef APP-PLUS || MP-WEIXIN || QUICKAPP-WEBVIEW || HARMONY
|
||||
// #ifdef APP-PLUS || MP
|
||||
"navigationStyle": "custom",
|
||||
// #endif
|
||||
"navigationBarTitleText": "",
|
||||
|
||||
@@ -56,10 +56,11 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品视频 -->
|
||||
<!-- 商品视频:objectFit=contain 保留原始宽高比,在固定高度的图片容器内居中播放 -->
|
||||
<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"></video>
|
||||
:poster="$util.img(goodsSkuDetail.sku_image, { size: 'big' })" objectFit="contain"
|
||||
playsinline webkit-playsinline x5-playsinline></video>
|
||||
</view>
|
||||
|
||||
<!-- 切换视频、图片 -->
|
||||
|
||||
@@ -603,7 +603,7 @@ export default {
|
||||
uni.setStorageSync('selectedProduct', productInfo);
|
||||
|
||||
uni.navigateTo({
|
||||
url: '/pages_tool/ai-chat/index'
|
||||
url: this.$util.PAGE_KEY_MAP.PAGE_AI_CHAT
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- 聊天内容区域:从导航栏下方到容器底部 -->
|
||||
<view class="chat-content">
|
||||
<view class="chat-content" :style="{ bottom: tabBarHeight }">
|
||||
<ai-chat-message
|
||||
ref="chat"
|
||||
:initial-messages="initialMessages"
|
||||
@@ -68,6 +68,15 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部导航栏(仅在 tabBar 场景下显示):
|
||||
通过 golbalConfig mixin 的 shouldShowTabBar 控制 v-show,
|
||||
组件内部已绑定 data-component-name="diy-bottom-nav"。
|
||||
- 后端把 ai-chat 配为底部导航栏项 → 用户点击该项进入 → tabBar 显示并高亮对应项
|
||||
- 浮动按钮(diy-float-btn)进入时 onLoad commit setBottomNavHidden(true)
|
||||
→ shouldShowTabBar=false → tabBar 隐藏
|
||||
- 离开页面(onUnload)还原 bottomNavHidden → 不污染其他页面 -->
|
||||
<diy-bottom-nav></diy-bottom-nav>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -87,6 +96,9 @@ export default {
|
||||
return {
|
||||
initialMessages: [],
|
||||
fallbackWelcome: '',
|
||||
// AI Agent 通用入参(链接系统透传;老客服入口不传也能跑,默认 agent_aikefu)
|
||||
agentId: 'agent_aikefu',
|
||||
pageTitle: '',
|
||||
|
||||
// --- 系统导航栏/状态栏相关 ---
|
||||
navBarHeight: 44, // uni-page-head 系统导航栏,默认高度为44px
|
||||
@@ -105,7 +117,15 @@ export default {
|
||||
keyboardHeight: 0,
|
||||
|
||||
// 事件处理器引用(用于清理)
|
||||
safeEventHandlers: new Map()
|
||||
safeEventHandlers: new Map(),
|
||||
|
||||
// 是否从浮动按钮进入
|
||||
// 用来在运行时区分两种场景:
|
||||
// 1) 后台把 ai-chat 配为底部导航栏 tabBar 项 → 用户点击该项进入 → 底部 tabBar 显示并高亮
|
||||
// 2) 浮动按钮(diy-float-btn)的链接指向 ai-chat → 用户点击浮动按钮进入 → 底部 tabBar 隐藏
|
||||
// 通过 onLoad(options).from 判定;同时会通过 $store.state.bottomNavHidden 临时隐藏 tabBar,
|
||||
// 并在 onUnload 还原,避免影响其他页面。
|
||||
isFromFloatBtn: false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -127,6 +147,21 @@ export default {
|
||||
async onLoad(options) {
|
||||
this.fallbackWelcome = this.$lang('ai-chat.index.fallbackWelcome');
|
||||
|
||||
// ====== AI Agent 通用入参(链接系统 -> 通用 agent_id/title) ======
|
||||
// 老客服入口不传 options 也能跑(智能客服是首个 agent,默认 agent_aikefu)。
|
||||
// 新增 AI 销售/HR/...智能体只需在 link.page_params.agent_id 传不同值,
|
||||
// 当前 onShow/initChat 不区分 agent 行为,后续按业务扩展按 agentId 路由即可。
|
||||
try {
|
||||
this.agentId = (options && options.agent_id) || 'agent_aikefu';
|
||||
const rawTitle = (options && options.title) || this.$lang('ai-chat.index.title');
|
||||
this.pageTitle = decodeURIComponent(rawTitle);
|
||||
// 同步 agentId 给 ai-service,后续 sendMessage / WS auth 自动带新 agent_id
|
||||
try { aiService.setAgentId(this.agentId); } catch (e) { /* 兼容旧 ai-service 版本 */ }
|
||||
} catch (e) {
|
||||
this.agentId = 'agent_aikefu';
|
||||
this.pageTitle = this.$lang('ai-chat.index.title');
|
||||
}
|
||||
|
||||
// ✅ 修改:取消权限校验,允许所有客服类型访问此页面
|
||||
// const hasAccess = this.checkAccessPermission();
|
||||
// if (!hasAccess) {
|
||||
@@ -134,7 +169,27 @@ export default {
|
||||
// return;
|
||||
// }
|
||||
|
||||
this.$langConfig.title(this.$lang('ai-chat.index.title'));
|
||||
this.$langConfig.title(this.pageTitle || this.$lang('ai-chat.index.title'));
|
||||
|
||||
// ====== 区分底部导航栏/浮动按钮两种进入来源 ======
|
||||
// diy-float-btn 跳转时会在 link 上注入 __dyu=FloatBtn
|
||||
// (见 diy-float-btn.vue#onFloatBtnClick)
|
||||
// 这种情况应当隐藏底部 tabBar(AI 客服页是全屏聊天体验,不应被 tabBar 占用屏幕)
|
||||
// 而点击底部导航栏 tabBar 项进入时,通常不会携带 __dyu query,
|
||||
// 此时 tabBar 正常显示并由 checkTabBarActive 步骤2精确命中高亮当前项。
|
||||
// 用 __dyu 这种 3 字符 + 双下划线前缀的命名,几乎不可能与业务 page_params 冲突
|
||||
// (业务方不会使用 __ 开头的短变量名)。
|
||||
const dyuEntry = (options && options.__dyu) || '';
|
||||
if (dyuEntry === 'FloatBtn') {
|
||||
this.isFromFloatBtn = true;
|
||||
// 临时隐藏底部导航栏;onUnload 会还原。
|
||||
try {
|
||||
this.$store.commit('setBottomNavHidden', true);
|
||||
} catch (e) { /* 兼容没有该 mutation 的旧 store */ }
|
||||
} else {
|
||||
this.isFromFloatBtn = false;
|
||||
}
|
||||
|
||||
this.initChat();
|
||||
},
|
||||
|
||||
@@ -180,6 +235,15 @@ export default {
|
||||
console.log(this.$lang('ai-chat.index.logOnUnloadTriggered'));
|
||||
this.cleanup();
|
||||
aiService.disconnectWebSocket();
|
||||
|
||||
// 还原底部导航栏显示状态:如果 onLoad 时因为来自浮动按钮而临时隐藏,
|
||||
// 必须在页面卸载时还原,否则返回到 tabBar 页时 tabBar 会保持隐藏状态。
|
||||
if (this.isFromFloatBtn) {
|
||||
try {
|
||||
this.$store.commit('setBottomNavHidden', false);
|
||||
} catch (e) { /* 兼容没有该 mutation 的旧 store */ }
|
||||
this.isFromFloatBtn = false;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -647,7 +711,9 @@ export default {
|
||||
/* #endif */
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* bottom 由模板动态绑定到 tabBarHeight:
|
||||
- 浮动按钮入口 / 其他隐藏 tabBar 场景:0rpx,占满到底部
|
||||
- tabBar 项入口:tabBarHeight,避免被底部 tabBar 遮挡聊天内容 */
|
||||
}
|
||||
|
||||
/* 底部tabBar占位样式 */
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<view :style="themeColor" class="harmony-root">
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-HARMONY -->
|
||||
<view :style="themeColor">
|
||||
<!-- #endif -->
|
||||
<!--
|
||||
自绘导航栏(仅在 navigationStyle:custom 的端渲染:APP / 微信小程序 / 华为快应用 / 鸿蒙元服务)
|
||||
- pages.json 已为这几端开启 navigationStyle:custom,系统不再提供原生 navbar
|
||||
@@ -23,6 +28,19 @@
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<block v-if="detail">
|
||||
<!-- 鸿蒙元服务:用原生 scroll-view 包裹表单,替代 CSS overflow 滚动(ArkUI 不识别 WebKit overflow 语义)。
|
||||
根容器 harmony-root 占满 100vh 并 flex 列布局,navbar 占顶部固定高度,page flex:1 占剩余,scroll-view 在其内滚动。
|
||||
提交按钮由 diy-form 内部渲染,处于 scroll-view 内,用户可滚到末尾点击。 -->
|
||||
<!-- #ifdef MP-HARMONY -->
|
||||
<view class="page harmony-page">
|
||||
<scroll-view scroll-y="true" :show-scrollbar="false" class="form-scroll">
|
||||
<view class="system-form-wrap">
|
||||
<diy-form ref="form" :value="diyValue" dataMode="raw" :onSubmit="handleSubmit" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef MP-HARMONY -->
|
||||
<view class="page">
|
||||
<view class="system-form-wrap">
|
||||
<!-- 复用 diy-form:banner / tip / 表单字段 / 提交按钮 全部由其内部渲染,样式(bannerPaddingX/tipColor/submitBgColor 等)由 form_config 控制。
|
||||
@@ -32,6 +50,7 @@
|
||||
<diy-form ref="form" :value="diyValue" dataMode="raw" :onSubmit="handleSubmit" />
|
||||
</view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</block>
|
||||
<block v-else>
|
||||
<ns-empty :text="complete ? $lang('form.formdata.submitSuccess') : $lang('form.formdata.noFormInfo')"
|
||||
@@ -420,10 +439,49 @@ page {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.system-form-wrap {
|
||||
overflow: hidden;
|
||||
margin: 0 0 60rpx 0;
|
||||
// ===== 非鸿蒙端(H5 / 微信小程序 / App):保留原有 WebKit 滚动 / 安全区处理 =====
|
||||
// 鸿蒙走下方 MP-HARMONY 专属的 scroll-view 方案,不编译这些 WebKit 属性
|
||||
/* #ifdef H5 || MP-WEIXIN || APP-PLUS */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.system-form-wrap {
|
||||
overflow: visible;
|
||||
margin: 0 0 60rpx 0;
|
||||
padding-bottom: env(safe-area-inset-bottom, 40rpx);
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
// ===== 鸿蒙元服务:用原生 scroll-view 做滚动容器 =====
|
||||
// ArkUI 不识别 WebKit 的 overflow / -webkit-overflow-scrolling,必须用 scroll-view 组件(编译为 ArkUI Scroll)。
|
||||
// 根容器 harmony-root 占满 100vh 并 flex 列布局,navbar 占顶部固定高度,page flex:1 占剩余空间,
|
||||
// scroll-view 在 page 内 flex:1 获得确定高度后内部滚动。提交按钮由 diy-form 渲染在 scroll-view 内,可滚动到。
|
||||
/* #ifdef MP-HARMONY */
|
||||
.harmony-root {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.harmony-page {
|
||||
flex: 1;
|
||||
min-height: 0; // 允许内容收缩,内部 scroll-view 才能拿到确定高度并正确滚动
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.form-scroll {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
.system-form-wrap {
|
||||
// 不再用 overflow:hidden 裁剪;提交按钮在 scroll-view 内,滚动可见
|
||||
overflow: visible;
|
||||
margin: 0 0 60rpx 0;
|
||||
padding-bottom: 40rpx; // 鸿蒙不识别 env(safe-area-inset-bottom),固定值兜底
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
// 穿透 diy-form 包装层:消除其自带的 10rpx 圆角与白色背景,
|
||||
// 让 form 内容区域保持与原始 ns-newform 接入时一致的视觉
|
||||
|
||||
Reference in New Issue
Block a user