web
This commit is contained in:
588
web/assets/js/api.js
Normal file
588
web/assets/js/api.js
Normal file
@@ -0,0 +1,588 @@
|
||||
// API接口调用模块
|
||||
|
||||
// 环境检测函数
|
||||
function getAPIBaseURL() {
|
||||
// 优先使用 localStorage 中的手动配置(用于调试)
|
||||
const manualBaseURL = localStorage.getItem('API_BASE_URL');
|
||||
if (manualBaseURL) {
|
||||
console.log('[API] 使用手动配置的后端地址:', manualBaseURL);
|
||||
return manualBaseURL;
|
||||
}
|
||||
|
||||
const hostname = window.location.hostname;
|
||||
const protocol = window.location.protocol;
|
||||
|
||||
// 开发环境判断:localhost 或 127.0.0.1
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
return 'http://localhost:8081/api/v1';
|
||||
}
|
||||
|
||||
// 生产环境:根据域名映射API地址
|
||||
// vizee.cn 域名使用 https://tral.cc/api/v1
|
||||
if (hostname.includes('vizee.cn')) {
|
||||
console.log('[API] 检测到 vizee.cn 域名,使用 tral.cc API');
|
||||
return 'https://tral.cc/api/v1';
|
||||
}
|
||||
|
||||
// gvizee.com 域名使用相对路径,由 Nginx 代理到后端
|
||||
if (hostname.includes('gvizee.com')) {
|
||||
console.log('[API] 检测到 gvizee.com 域名,使用 Nginx 代理');
|
||||
// 使用相对路径,自动继承当前页面的协议(HTTPS)
|
||||
// Nginx 会将 /api/v1 请求代理到 http://104.244.91.212:8060/api/v1
|
||||
return '/api/v1';
|
||||
}
|
||||
|
||||
// 默认使用 tral.cc
|
||||
console.log('[API] 使用默认 API 地址: tral.cc');
|
||||
return 'https://tral.cc/api/v1';
|
||||
}
|
||||
|
||||
// API配置
|
||||
const API_CONFIG = {
|
||||
baseURL: getAPIBaseURL(),
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
|
||||
// 环境信息
|
||||
env: {
|
||||
isDevelopment: window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1',
|
||||
isProduction: window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1'
|
||||
}
|
||||
};
|
||||
|
||||
// 控制台输出当前环境信息
|
||||
console.log('[API] 当前环境:', API_CONFIG.env.isDevelopment ? '开发环境' : '生产环境');
|
||||
console.log('[API] 后端地址:', API_CONFIG.baseURL);
|
||||
|
||||
// 全局辅助函数(用于开发调试)
|
||||
window.API_DEBUG = {
|
||||
// 设置后端地址
|
||||
setBaseURL: function(url) {
|
||||
localStorage.setItem('API_BASE_URL', url);
|
||||
console.log('[API] 后端地址已设置为:', url);
|
||||
console.log('[API] 请刷新页面使配置生效');
|
||||
},
|
||||
|
||||
// 重置为自动检测
|
||||
resetBaseURL: function() {
|
||||
localStorage.removeItem('API_BASE_URL');
|
||||
console.log('[API] 已重置为自动检测模式');
|
||||
console.log('[API] 请刷新页面使配置生效');
|
||||
},
|
||||
|
||||
// 查看当前配置
|
||||
getConfig: function() {
|
||||
return {
|
||||
baseURL: API_CONFIG.baseURL,
|
||||
environment: API_CONFIG.env.isDevelopment ? 'development' : 'production',
|
||||
manualConfig: localStorage.getItem('API_BASE_URL') || 'auto',
|
||||
currentURL: window.location.href
|
||||
};
|
||||
},
|
||||
|
||||
// 测试连接
|
||||
testConnection: function() {
|
||||
console.log('[API] 测试后端连接...');
|
||||
fetch(API_CONFIG.baseURL.replace('/api/v1', '/health'))
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('[API] ✅ 后端连接成功:', data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[API] ❌ 后端连接失败:', error.message);
|
||||
console.log('[API] 请确认后端服务已启动');
|
||||
});
|
||||
},
|
||||
|
||||
// 使用说明
|
||||
help: function() {
|
||||
console.log(`
|
||||
=== API调试工具使用说明 ===
|
||||
|
||||
域名映射规则:
|
||||
- vizee.cn 域名使用: https://tral.cc/api/v1
|
||||
- gvizee.com 域名使用: https://104.244.91.212:8060/api/v1
|
||||
- localhost/127.0.0.1 使用: http://localhost:8081/api/v1
|
||||
- 其他域名默认使用: https://tral.cc/api/v1
|
||||
|
||||
1. 查看当前配置:
|
||||
API_DEBUG.getConfig()
|
||||
|
||||
2. 手动设置后端地址:
|
||||
API_DEBUG.setBaseURL('http://localhost:8080/api/v1')
|
||||
API_DEBUG.setBaseURL('https://api.example.com/api/v1')
|
||||
|
||||
3. 重置为自动检测:
|
||||
API_DEBUG.resetBaseURL()
|
||||
|
||||
4. 测试后端连接:
|
||||
API_DEBUG.testConnection()
|
||||
|
||||
5. 查看帮助:
|
||||
API_DEBUG.help()
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
// 启动时提示
|
||||
if (API_CONFIG.env.isDevelopment) {
|
||||
console.log('[API] 💡 开发提示:在控制台输入 API_DEBUG.help() 查看调试工具使用说明');
|
||||
}
|
||||
|
||||
// API模块
|
||||
const API = {
|
||||
// 获取Token
|
||||
getToken: function() {
|
||||
const user = localStorage.getItem('currentUser');
|
||||
if (user) {
|
||||
try {
|
||||
const userData = JSON.parse(user);
|
||||
return userData.token || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
// 设置Token
|
||||
setToken: function(token) {
|
||||
const user = localStorage.getItem('currentUser');
|
||||
if (user) {
|
||||
try {
|
||||
const userData = JSON.parse(user);
|
||||
userData.token = token;
|
||||
localStorage.setItem('currentUser', JSON.stringify(userData));
|
||||
} catch (e) {
|
||||
console.error('设置Token失败:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 通用请求方法
|
||||
request: function(options) {
|
||||
const url = API_CONFIG.baseURL + options.url;
|
||||
const method = options.method || 'GET';
|
||||
const data = options.data || null;
|
||||
const headers = Object.assign({}, API_CONFIG.headers, options.headers || {});
|
||||
|
||||
// 添加认证Token
|
||||
const token = this.getToken();
|
||||
if (token) {
|
||||
headers['Authorization'] = 'Bearer ' + token;
|
||||
}
|
||||
|
||||
const ajaxOptions = {
|
||||
url: url,
|
||||
method: method,
|
||||
headers: headers,
|
||||
timeout: API_CONFIG.timeout,
|
||||
xhrFields: {
|
||||
withCredentials: true
|
||||
}
|
||||
};
|
||||
|
||||
// 添加数据
|
||||
if (data) {
|
||||
if (method === 'GET') {
|
||||
ajaxOptions.data = data;
|
||||
} else {
|
||||
ajaxOptions.data = JSON.stringify(data);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回Promise
|
||||
return new Promise((resolve, reject) => {
|
||||
$.ajax(ajaxOptions)
|
||||
.done(function(response) {
|
||||
// 检查响应格式
|
||||
if (response && response.code === 200) {
|
||||
resolve(response.data);
|
||||
} else {
|
||||
reject(response);
|
||||
}
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
// 处理错误
|
||||
const errorMsg = xhr.responseJSON?.message || error || '网络请求失败';
|
||||
|
||||
// 401未授权,提示并跳转登录
|
||||
if (xhr.status === 401) {
|
||||
// 判断是未登录还是登录过期
|
||||
const hadToken = !!localStorage.getItem('currentUser');
|
||||
const message = hadToken ? '登录已过期,请重新登录' : '请先登录';
|
||||
|
||||
console.log('[401 拦截]', message);
|
||||
|
||||
// 清除登录状态
|
||||
localStorage.removeItem('currentUser');
|
||||
|
||||
// 使用 Toast.alert 显示提示(iOS 风格)
|
||||
if (typeof Toast !== 'undefined' && Toast.alert) {
|
||||
Toast.alert(message).then(() => {
|
||||
// 用户点击确定后跳转
|
||||
handleRedirectToLogin();
|
||||
});
|
||||
} else if (typeof Toast !== 'undefined' && Toast.warning) {
|
||||
// 降级使用 warning
|
||||
Toast.warning(message);
|
||||
setTimeout(handleRedirectToLogin, 2000);
|
||||
} else {
|
||||
// 最后降级使用原生 alert
|
||||
alert(message);
|
||||
handleRedirectToLogin();
|
||||
}
|
||||
|
||||
// 跳转逻辑封装
|
||||
function handleRedirectToLogin() {
|
||||
console.log('[401 跳转] 准备跳转登录页');
|
||||
// 记录当前页面,登录后可以返回(排除登录页和用户中心)
|
||||
const currentPage = window.location.pathname + window.location.search;
|
||||
const shouldRedirect = !currentPage.includes('login.html') &&
|
||||
!currentPage.includes('user-center.html');
|
||||
|
||||
if (shouldRedirect) {
|
||||
localStorage.setItem('redirectUrl', currentPage);
|
||||
console.log('[401 跳转] 保存 redirectUrl:', currentPage);
|
||||
} else {
|
||||
// 确保清除之前的redirectUrl
|
||||
localStorage.removeItem('redirectUrl');
|
||||
console.log('[401 跳转] 不保存 redirectUrl');
|
||||
}
|
||||
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
|
||||
reject({
|
||||
code: 401,
|
||||
message: message,
|
||||
data: xhr.responseJSON
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
reject({
|
||||
code: xhr.status,
|
||||
message: errorMsg,
|
||||
data: xhr.responseJSON
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// GET请求
|
||||
get: function(url, params) {
|
||||
return this.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
data: params
|
||||
});
|
||||
},
|
||||
|
||||
// POST请求
|
||||
post: function(url, data) {
|
||||
return this.request({
|
||||
url: url,
|
||||
method: 'POST',
|
||||
data: data
|
||||
});
|
||||
},
|
||||
|
||||
// PUT请求
|
||||
put: function(url, data) {
|
||||
return this.request({
|
||||
url: url,
|
||||
method: 'PUT',
|
||||
data: data
|
||||
});
|
||||
},
|
||||
|
||||
// DELETE请求
|
||||
delete: function(url, data) {
|
||||
return this.request({
|
||||
url: url,
|
||||
method: 'DELETE',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 价格工具函数
|
||||
const PriceUtils = {
|
||||
/**
|
||||
* 将后端返回的价格(分)转换为前端显示的价格(元)
|
||||
* @param {number} price - 以分为单位的价格
|
||||
* @returns {number} - 以元为单位的价格
|
||||
*/
|
||||
fenToYuan: function(price) {
|
||||
return (price || 0) / 100;
|
||||
},
|
||||
|
||||
/**
|
||||
* 将前端输入的价格(元)转换为后端需要的价格(分)
|
||||
* @param {number} price - 以元为单位的价格
|
||||
* @returns {number} - 以分为单位的价格
|
||||
*/
|
||||
yuanToFen: function(price) {
|
||||
return Math.round((price || 0) * 100);
|
||||
},
|
||||
|
||||
/**
|
||||
* 格式化价格显示(带货币符号)
|
||||
* @param {number} price - 以分为单位的价格
|
||||
* @param {string} currency - 货币符号,默认为¥
|
||||
* @returns {string} - 格式化后的价格字符串
|
||||
*/
|
||||
formatPrice: function(price, currency = '¥') {
|
||||
return currency + this.fenToYuan(price).toFixed(2);
|
||||
}
|
||||
};
|
||||
|
||||
// 具体业务API
|
||||
const UserAPI = {
|
||||
// 用户登录(邮箱密码)
|
||||
login: function(email, password) {
|
||||
return API.post('/users/email-login', {
|
||||
email: email,
|
||||
password: password
|
||||
});
|
||||
},
|
||||
|
||||
// 用户注册(邮箱密码)
|
||||
register: function(email, password, nickname) {
|
||||
return API.post('/users/email-register', {
|
||||
email: email,
|
||||
password: password,
|
||||
nickname: nickname
|
||||
});
|
||||
},
|
||||
|
||||
// 获取用户信息
|
||||
getProfile: function() {
|
||||
return API.get('/users/profile');
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
updateProfile: function(data) {
|
||||
return API.put('/users/profile', data);
|
||||
},
|
||||
|
||||
// 获取地址列表
|
||||
getAddresses: function() {
|
||||
return API.get('/users/addresses');
|
||||
},
|
||||
|
||||
// 添加地址
|
||||
addAddress: function(data) {
|
||||
return API.post('/users/addresses', data);
|
||||
},
|
||||
|
||||
// 更新地址
|
||||
updateAddress: function(id, data) {
|
||||
return API.put('/users/addresses/' + id, data);
|
||||
},
|
||||
|
||||
// 删除地址
|
||||
deleteAddress: function(id) {
|
||||
return API.delete('/users/addresses/' + id);
|
||||
}
|
||||
};
|
||||
|
||||
const ProductAPI = {
|
||||
// 获取商品列表
|
||||
getList: function(params) {
|
||||
return API.get('/products', params);
|
||||
},
|
||||
|
||||
// 获取商品详情
|
||||
getDetail: function(id) {
|
||||
return API.get('/products/' + id);
|
||||
},
|
||||
|
||||
// 获取热门商品
|
||||
getHot: function() {
|
||||
return API.get('/products/hot');
|
||||
},
|
||||
|
||||
// 获取推荐商品
|
||||
getRecommend: function() {
|
||||
return API.get('/products/recommend');
|
||||
},
|
||||
|
||||
// 搜索商品
|
||||
search: function(keyword) {
|
||||
return API.get('/products/search', { keyword: keyword });
|
||||
},
|
||||
|
||||
// 获取商品评价
|
||||
getReviews: function(id, params) {
|
||||
return API.get('/products/' + id + '/reviews', params);
|
||||
},
|
||||
|
||||
// 获取分类列表
|
||||
getCategories: function() {
|
||||
return API.get('/products/categories', { platform: 'web' });
|
||||
}
|
||||
};
|
||||
|
||||
const CartAPI = {
|
||||
// 获取购物车
|
||||
getCart: function() {
|
||||
return API.get('/cart');
|
||||
},
|
||||
|
||||
// 添加到购物车
|
||||
addToCart: function(productId, skuId, quantity) {
|
||||
return API.post('/cart', {
|
||||
product_id: productId,
|
||||
sku_id: skuId,
|
||||
quantity: quantity
|
||||
});
|
||||
},
|
||||
|
||||
// 更新购物车数量
|
||||
updateQuantity: function(productId, quantity) {
|
||||
return API.put('/cart/' + productId, {
|
||||
quantity: quantity
|
||||
});
|
||||
},
|
||||
|
||||
// 删除购物车商品
|
||||
removeItem: function(productId) {
|
||||
return API.delete('/cart/' + productId);
|
||||
},
|
||||
|
||||
// 清空购物车
|
||||
clearCart: function() {
|
||||
return API.delete('/cart');
|
||||
},
|
||||
|
||||
// 获取购物车数量
|
||||
getCount: function() {
|
||||
return API.get('/cart/count');
|
||||
}
|
||||
};
|
||||
|
||||
const OrderAPI = {
|
||||
// 创建订单
|
||||
createOrder: function(data) {
|
||||
return API.post('/orders', data);
|
||||
},
|
||||
|
||||
// 获取订单列表
|
||||
getList: function(params) {
|
||||
return API.get('/orders', params);
|
||||
},
|
||||
|
||||
// 获取订单详情
|
||||
getDetail: function(id) {
|
||||
return API.get('/orders/' + id);
|
||||
},
|
||||
|
||||
// 取消订单
|
||||
cancelOrder: function(id, reason) {
|
||||
return API.post('/orders/' + id + '/cancel', {
|
||||
reason: reason
|
||||
});
|
||||
},
|
||||
|
||||
// 确认收货
|
||||
confirmReceive: function(id) {
|
||||
return API.post('/orders/' + id + '/confirm');
|
||||
}
|
||||
};
|
||||
|
||||
const BannerAPI = {
|
||||
// 获取轮播图
|
||||
getList: function() {
|
||||
return API.get('/banners');
|
||||
}
|
||||
};
|
||||
|
||||
const CouponAPI = {
|
||||
// 获取可用优惠券
|
||||
getAvailable: function() {
|
||||
return API.get('/coupons');
|
||||
},
|
||||
|
||||
// 获取我的优惠券
|
||||
getMyCoupons: function(status) {
|
||||
return API.get('/coupons/user', { status: status });
|
||||
},
|
||||
|
||||
// 领取优惠券
|
||||
receive: function(id) {
|
||||
return API.post('/coupons/' + id + '/receive');
|
||||
}
|
||||
};
|
||||
|
||||
const CommentAPI = {
|
||||
// 获取商品评论
|
||||
getProductComments: function(productId, params) {
|
||||
return API.get('/comments/products/' + productId, params);
|
||||
},
|
||||
|
||||
// 获取评论统计
|
||||
getStats: function(productId) {
|
||||
return API.get('/comments/products/' + productId + '/stats');
|
||||
},
|
||||
|
||||
// 创建评论
|
||||
create: function(data) {
|
||||
return API.post('/comments', data);
|
||||
}
|
||||
};
|
||||
|
||||
const UploadAPI = {
|
||||
// 上传图片
|
||||
uploadImage: function(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = API.getToken();
|
||||
|
||||
$.ajax({
|
||||
url: API_CONFIG.baseURL + '/upload/image',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
xhrFields: {
|
||||
withCredentials: true
|
||||
}
|
||||
})
|
||||
.done(function(response) {
|
||||
if (response && response.code === 200) {
|
||||
resolve(response.data);
|
||||
} else {
|
||||
reject(response);
|
||||
}
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
reject({
|
||||
code: xhr.status,
|
||||
message: xhr.responseJSON?.message || error,
|
||||
data: xhr.responseJSON
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const LiveStreamAPI = {
|
||||
// 获取启用的投流源
|
||||
getActiveLiveStreams: function() {
|
||||
return API.get('/livestreams');
|
||||
},
|
||||
|
||||
// 增加观看次数
|
||||
incrementViewCount: function(id) {
|
||||
return API.post('/livestreams/' + id + '/view');
|
||||
}
|
||||
};
|
||||
419
web/assets/js/cart.js
Normal file
419
web/assets/js/cart.js
Normal file
@@ -0,0 +1,419 @@
|
||||
// Cart Page JavaScript - 使用真实API
|
||||
|
||||
console.log('=== cart.js 已加载 ===');
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log('=== cart.js $(document).ready ===');
|
||||
initCartPage();
|
||||
});
|
||||
|
||||
function initCartPage() {
|
||||
console.log('=== initCartPage 开始 ===');
|
||||
loadCartItems();
|
||||
loadRecommendations();
|
||||
bindCartEvents();
|
||||
console.log('=== initCartPage 完成 ===');
|
||||
|
||||
// 监听语言切换
|
||||
$(document).on('languageChanged', function() {
|
||||
loadCartItems();
|
||||
loadRecommendations();
|
||||
});
|
||||
}
|
||||
|
||||
// 加载购物车商品 - 从API获取
|
||||
function loadCartItems() {
|
||||
// 调用后端API获取购物车数据
|
||||
API.get('/cart')
|
||||
.then(data => {
|
||||
console.log('购物车数据:', data);
|
||||
|
||||
const cartItems = data.items || [];
|
||||
|
||||
if (cartItems.length === 0) {
|
||||
$('#cartContent').hide();
|
||||
$('#cartEmpty').show();
|
||||
updateCartCount(0);
|
||||
return;
|
||||
}
|
||||
|
||||
$('#cartEmpty').hide();
|
||||
$('#cartContent').show();
|
||||
|
||||
renderCartItems(cartItems);
|
||||
updateCartCount(data.total_quantity || cartItems.reduce((sum, item) => sum + item.quantity, 0));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载购物车失败:', error);
|
||||
Toast.error(error.message || '加载购物车失败');
|
||||
$('#cartContent').hide();
|
||||
$('#cartEmpty').show();
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染购物车商品列表
|
||||
function renderCartItems(cartItems) {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
// 生成购物车表格HTML
|
||||
let cartHtml = `
|
||||
<table class="cart-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="selectAll" ${isAllSelected(cartItems) ? 'checked' : ''}></th>
|
||||
<th>${i18n.t('product')}</th>
|
||||
<th>${i18n.t('price')}</th>
|
||||
<th>${i18n.t('quantity')}</th>
|
||||
<th>${i18n.t('total')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
// 添加商品行
|
||||
cartItems.forEach(item => {
|
||||
const product = item.product || {};
|
||||
const sku = item.sku || {};
|
||||
|
||||
const itemName = product.name || '未知商品';
|
||||
const itemImage = product.main_image || product.image || 'https://picsum.photos/200/200?random=default';
|
||||
|
||||
// 价格转换:分 → 元
|
||||
const itemPrice = PriceUtils.fenToYuan(sku.price || product.price || 0);
|
||||
const itemTotal = PriceUtils.fenToYuan((sku.price || product.price || 0) * item.quantity);
|
||||
|
||||
// 规格信息
|
||||
let specsHtml = '';
|
||||
if (sku.spec_values && Object.keys(sku.spec_values).length > 0) {
|
||||
specsHtml = '<div class="cart-product-specs">';
|
||||
for (const [key, value] of Object.entries(sku.spec_values)) {
|
||||
specsHtml += `<span class="spec-tag">${key}: ${value}</span>`;
|
||||
}
|
||||
specsHtml += '</div>';
|
||||
}
|
||||
|
||||
cartHtml += `
|
||||
<tr data-cart-id="${item.id}" data-product-id="${item.product_id}" data-sku-id="${item.sku_id || 0}">
|
||||
<td>
|
||||
<input type="checkbox" class="item-checkbox" data-cart-id="${item.id}" ${item.selected ? 'checked' : ''}>
|
||||
</td>
|
||||
<td data-label="${i18n.t('product')}">
|
||||
<div class="cart-product">
|
||||
<div class="cart-product-image">
|
||||
<img src="${itemImage}" alt="${itemName}">
|
||||
</div>
|
||||
<div class="cart-product-info">
|
||||
<div class="cart-product-name">
|
||||
<a href="product-detail.html?id=${item.product_id}">${itemName}</a>
|
||||
</div>
|
||||
${specsHtml}
|
||||
<button class="cart-product-remove" data-cart-id="${item.id}" data-product-id="${item.product_id}" data-sku-id="${item.sku_id || 0}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
${i18n.t('remove')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="${i18n.t('price')}" class="cart-price">
|
||||
¥${itemPrice.toFixed(2)}
|
||||
</td>
|
||||
<td data-label="${i18n.t('quantity')}" class="cart-quantity">
|
||||
<div class="quantity-selector">
|
||||
<button class="qty-btn minus" data-product-id="${item.product_id}" data-sku-id="${item.sku_id || 0}">-</button>
|
||||
<input type="number" class="qty-input" value="${item.quantity}" min="1" max="99" data-product-id="${item.product_id}" data-sku-id="${item.sku_id || 0}">
|
||||
<button class="qty-btn plus" data-product-id="${item.product_id}" data-sku-id="${item.sku_id || 0}">+</button>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="${i18n.t('total')}" class="cart-total-price">
|
||||
¥${itemTotal.toFixed(2)}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
// 计算已选商品的小计和总计
|
||||
const selectedItems = cartItems.filter(item => item.selected);
|
||||
const subtotal = calculateSubtotal(selectedItems);
|
||||
const total = subtotal; // 暂不计算折扣
|
||||
|
||||
cartHtml += `
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="cart-summary">
|
||||
<h3>${i18n.t('cart_totals')}</h3>
|
||||
<div class="summary-row">
|
||||
<span>${i18n.t('subtotal')}</span>
|
||||
<span class="price" id="subtotalAmount">¥${PriceUtils.fenToYuan(subtotal).toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span>${i18n.t('shipping')}</span>
|
||||
<span class="price" id="shippingAmount">${i18n.t('free')}</span>
|
||||
</div>
|
||||
<div class="summary-row total">
|
||||
<span>${i18n.t('total')}</span>
|
||||
<span class="price" id="totalAmount">¥${PriceUtils.fenToYuan(total).toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="shipping-notice">
|
||||
${i18n.t('free_shipping_notice')}
|
||||
</div>
|
||||
<button class="btn btn-primary btn-checkout" id="checkoutBtn" ${selectedItems.length === 0 ? 'disabled' : ''}>
|
||||
${i18n.t('proceed_to_checkout')}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#cartContent').html(cartHtml);
|
||||
}
|
||||
|
||||
// 判断是否全选
|
||||
function isAllSelected(cartItems) {
|
||||
return cartItems.length > 0 && cartItems.every(item => item.selected);
|
||||
}
|
||||
|
||||
// 计算小计(已选商品)
|
||||
function calculateSubtotal(selectedItems) {
|
||||
return selectedItems.reduce((sum, item) => {
|
||||
const price = (item.sku && item.sku.price) || (item.product && item.product.price) || 0;
|
||||
return sum + (price * item.quantity);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// 加载推荐商品
|
||||
function loadRecommendations() {
|
||||
// 调用商品列表API获取推荐商品
|
||||
API.get('/products/list', { page: 1, size: 4 })
|
||||
.then(data => {
|
||||
const products = data.products || data.list || [];
|
||||
renderRecommendations(products.slice(0, 4));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载推荐商品失败:', error);
|
||||
// 失败时不显示推荐区域
|
||||
$('.recommendations-section').hide();
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染推荐商品
|
||||
function renderRecommendations(products) {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
let recommendationsHtml = '';
|
||||
|
||||
products.forEach(product => {
|
||||
const productName = product.name || '商品名称';
|
||||
const category = product.category_name || '商品分类';
|
||||
const currentPrice = PriceUtils.fenToYuan(product.price || 0);
|
||||
const originalPrice = product.original_price ? PriceUtils.fenToYuan(product.original_price) : null;
|
||||
const mainImage = product.main_image || product.image || 'https://picsum.photos/400/400?random=default';
|
||||
|
||||
recommendationsHtml += `
|
||||
<div class="recommendation-card">
|
||||
<div class="recommendation-image">
|
||||
<img src="${mainImage}" alt="${productName}">
|
||||
${product.stock > 0 ? '' : '<span class="recommendation-badge">售罄</span>'}
|
||||
</div>
|
||||
<div class="recommendation-info">
|
||||
<div class="recommendation-category">${category}</div>
|
||||
<div class="recommendation-name">${productName}</div>
|
||||
<div class="recommendation-price">
|
||||
<span class="price-current">¥${currentPrice.toFixed(2)}</span>
|
||||
${originalPrice ? `<span class="price-original">¥${originalPrice.toFixed(2)}</span>` : ''}
|
||||
</div>
|
||||
<button class="recommendation-add-btn" data-product-id="${product.id}" ${product.stock <= 0 ? 'disabled' : ''}>
|
||||
${product.stock > 0 ? i18n.t('add_to_cart') : '已售罄'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#recommendationsGrid').html(recommendationsHtml);
|
||||
}
|
||||
|
||||
// 绑定购物车事件
|
||||
function bindCartEvents() {
|
||||
console.log('=== 绑定购物车事件 ===');
|
||||
// 全选/取消全选
|
||||
$(document).off('change', '#selectAll').on('change', '#selectAll', function() {
|
||||
const selected = $(this).is(':checked');
|
||||
selectAllItems(selected);
|
||||
});
|
||||
|
||||
// 单个商品选择
|
||||
$(document).off('change', '.item-checkbox').on('change', '.item-checkbox', function() {
|
||||
const cartId = $(this).data('cart-id');
|
||||
const selected = $(this).is(':checked');
|
||||
selectCartItem(cartId, selected);
|
||||
});
|
||||
|
||||
// 数量增加
|
||||
$(document).off('click', '.qty-btn.plus').on('click', '.qty-btn.plus', function() {
|
||||
console.log('点击了+按钮');
|
||||
const productId = $(this).data('product-id');
|
||||
const skuId = $(this).data('sku-id');
|
||||
const $input = $(this).siblings('.qty-input');
|
||||
const currentQty = parseInt($input.val());
|
||||
console.log('+ 按钮参数:', { productId, skuId, currentQty });
|
||||
updateQuantity(productId, skuId, currentQty + 1);
|
||||
});
|
||||
|
||||
// 数量减少
|
||||
$(document).off('click', '.qty-btn.minus').on('click', '.qty-btn.minus', function() {
|
||||
console.log('点击了-按钮');
|
||||
const productId = $(this).data('product-id');
|
||||
const skuId = $(this).data('sku-id');
|
||||
const $input = $(this).siblings('.qty-input');
|
||||
const currentQty = parseInt($input.val());
|
||||
console.log('- 按钮参数:', { productId, skuId, currentQty });
|
||||
if (currentQty > 1) {
|
||||
updateQuantity(productId, skuId, currentQty - 1);
|
||||
}
|
||||
});
|
||||
|
||||
// 数量输入框变化
|
||||
$(document).off('change', '.qty-input').on('change', '.qty-input', function() {
|
||||
console.log('输入框数量变化');
|
||||
const productId = $(this).data('product-id');
|
||||
const skuId = $(this).data('sku-id');
|
||||
let newQuantity = parseInt($(this).val());
|
||||
console.log('输入框参数:', { productId, skuId, newQuantity });
|
||||
|
||||
if (isNaN(newQuantity) || newQuantity < 1) {
|
||||
newQuantity = 1;
|
||||
} else if (newQuantity > 99) {
|
||||
newQuantity = 99;
|
||||
}
|
||||
|
||||
$(this).val(newQuantity);
|
||||
updateQuantity(productId, skuId, newQuantity);
|
||||
});
|
||||
|
||||
// 移除商品
|
||||
$(document).off('click', '.cart-product-remove').on('click', '.cart-product-remove', function() {
|
||||
const productId = $(this).data('product-id');
|
||||
const skuId = $(this).data('sku-id');
|
||||
removeItem(productId, skuId);
|
||||
});
|
||||
|
||||
// 结算按钮
|
||||
$(document).off('click', '#checkoutBtn').on('click', '#checkoutBtn', function() {
|
||||
if (!$(this).prop('disabled')) {
|
||||
window.location.href = 'checkout.html';
|
||||
}
|
||||
});
|
||||
|
||||
// 添加推荐商品
|
||||
$(document).off('click', '.recommendation-add-btn').on('click', '.recommendation-add-btn', function() {
|
||||
const productId = $(this).data('product-id');
|
||||
if (!$(this).prop('disabled')) {
|
||||
addRecommendedProduct(productId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 全选/取消全选
|
||||
function selectAllItems(selected) {
|
||||
API.put('/cart/select-all', { selected: selected })
|
||||
.then(() => {
|
||||
loadCartItems();
|
||||
})
|
||||
.catch(error => {
|
||||
Toast.error(error.message || '操作失败');
|
||||
// 恢复选中状态
|
||||
$('#selectAll').prop('checked', !selected);
|
||||
});
|
||||
}
|
||||
|
||||
// 选择/取消选择单个商品
|
||||
function selectCartItem(cartId, selected) {
|
||||
API.put(`/cart/${cartId}/select`, { selected: selected })
|
||||
.then(() => {
|
||||
loadCartItems();
|
||||
})
|
||||
.catch(error => {
|
||||
Toast.error(error.message || '操作失败');
|
||||
// 恢复选中状态
|
||||
$(`.item-checkbox[data-cart-id="${cartId}"]`).prop('checked', !selected);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新商品数量
|
||||
function updateQuantity(productId, skuId, quantity) {
|
||||
console.log('updateQuantity 参数:', { productId, skuId, quantity });
|
||||
|
||||
if (!productId) {
|
||||
console.error('productId 不能为空');
|
||||
Toast.error('商品ID不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { quantity: quantity };
|
||||
if (skuId && skuId != 0) {
|
||||
data.sku_id = parseInt(skuId);
|
||||
}
|
||||
|
||||
const url = `/cart/${productId}`;
|
||||
console.log('更新购物车请求URL:', url, '数据:', data);
|
||||
|
||||
API.put(url, data)
|
||||
.then(() => {
|
||||
Toast.success('修改成功');
|
||||
loadCartItems();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('更新购物车失败:', error);
|
||||
Toast.error(error.message || '修改数量失败');
|
||||
loadCartItems(); // 刷新以恢复原数量
|
||||
});
|
||||
}
|
||||
|
||||
// 移除商品
|
||||
function removeItem(productId, skuId) {
|
||||
Toast.confirm({
|
||||
title: '确认删除',
|
||||
message: '确定要从购物车中删除该商品吗?',
|
||||
confirmText: '确定',
|
||||
cancelText: '取消'
|
||||
}).then(confirmed => {
|
||||
if (confirmed) {
|
||||
let url = `/cart/${productId}`;
|
||||
if (skuId && skuId != 0) {
|
||||
url += `?sku_id=${skuId}`;
|
||||
}
|
||||
|
||||
API.delete(url)
|
||||
.then(() => {
|
||||
Toast.success('已删除');
|
||||
loadCartItems();
|
||||
})
|
||||
.catch(error => {
|
||||
Toast.error(error.message || '删除失败');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加推荐商品到购物车
|
||||
function addRecommendedProduct(productId) {
|
||||
API.post('/cart', {
|
||||
product_id: parseInt(productId),
|
||||
quantity: 1
|
||||
})
|
||||
.then(() => {
|
||||
Toast.success(i18n.t('product_added_to_cart') || '已添加到购物车');
|
||||
loadCartItems();
|
||||
})
|
||||
.catch(error => {
|
||||
Toast.error(error.message || '添加失败');
|
||||
});
|
||||
}
|
||||
|
||||
// 更新购物车数量显示
|
||||
function updateCartCount(count) {
|
||||
$('#cartCount').text(count);
|
||||
$('.cart-count').text(count);
|
||||
}
|
||||
1425
web/assets/js/checkout.js
Normal file
1425
web/assets/js/checkout.js
Normal file
File diff suppressed because it is too large
Load Diff
16870
web/assets/js/china-regions.js
Normal file
16870
web/assets/js/china-regions.js
Normal file
File diff suppressed because it is too large
Load Diff
1029
web/assets/js/common.js
Normal file
1029
web/assets/js/common.js
Normal file
File diff suppressed because it is too large
Load Diff
584
web/assets/js/home.js
Normal file
584
web/assets/js/home.js
Normal file
@@ -0,0 +1,584 @@
|
||||
// 首页功能
|
||||
|
||||
// 加载轮播图
|
||||
function loadBanners() {
|
||||
console.log('=== 开始加载轮播图 ===');
|
||||
|
||||
// 调用后端轮播图 API
|
||||
API.get('/banners')
|
||||
.then(data => {
|
||||
console.log('轮播图 API 返回数据:', data);
|
||||
|
||||
// 支持多种数据格式
|
||||
let banners = [];
|
||||
if (Array.isArray(data)) {
|
||||
// 直接返回数组
|
||||
banners = data;
|
||||
} else if (data.data) {
|
||||
// 数据在 data.data 中
|
||||
banners = Array.isArray(data.data) ? data.data : (data.data.list || []);
|
||||
} else if (data.list) {
|
||||
// 数据在 data.list 中
|
||||
banners = data.list;
|
||||
} else if (data.banners) {
|
||||
// 数据在 data.banners 中
|
||||
banners = data.banners;
|
||||
}
|
||||
|
||||
console.log('解析到的轮播图:', banners, '数量:', banners.length);
|
||||
|
||||
if (banners.length === 0) {
|
||||
console.warn('没有可用的轮播图,使用默认轮播图');
|
||||
// 没有轮播图时,使用 HTML 中的默认轮播图
|
||||
heroSlider.init();
|
||||
return;
|
||||
}
|
||||
|
||||
renderBanners(banners);
|
||||
// 渲染完成后初始化轮播图
|
||||
heroSlider.init();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载轮播图失败:', error);
|
||||
// 失败时使用 HTML 中的默认轮播图,仍然初始化
|
||||
heroSlider.init();
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染轮播图
|
||||
function renderBanners(banners) {
|
||||
const bannersHtml = banners.map((banner, index) => {
|
||||
// 处理链接
|
||||
let link = '#';
|
||||
let hasLink = false;
|
||||
|
||||
if (banner.link_type && banner.link_value) {
|
||||
// link_type: 1=无链接, 2=商品详情, 3=分类页面, 4=外部链接
|
||||
if (banner.link_type === 2) {
|
||||
// 商品详情
|
||||
link = `product-detail.html?id=${banner.link_value}`;
|
||||
hasLink = true;
|
||||
} else if (banner.link_type === 3) {
|
||||
// 分类页面
|
||||
link = `home.html?category=${banner.link_value}`;
|
||||
hasLink = true;
|
||||
} else if (banner.link_type === 4) {
|
||||
// 外部链接
|
||||
link = banner.link_value;
|
||||
hasLink = true;
|
||||
}
|
||||
} else if (banner.link) {
|
||||
// 兼容旧的 link 字段
|
||||
link = banner.link;
|
||||
hasLink = true;
|
||||
}
|
||||
|
||||
const title = banner.title || `轮播图 ${index + 1}`;
|
||||
const subtitle = banner.description || banner.subtitle || '';
|
||||
const image = banner.image || `https://picsum.photos/1920/600?random=banner${index}`;
|
||||
|
||||
// 如果有链接,整个轮播图可点击
|
||||
if (hasLink) {
|
||||
return `
|
||||
<div class="slide ${index === 0 ? 'active' : ''}">
|
||||
<a href="${link}" class="slide-link" ${banner.link_type === 4 ? 'target="_blank" rel="noopener noreferrer"' : ''}>
|
||||
<img src="${image}" alt="${title}" loading="${index === 0 ? 'eager' : 'lazy'}">
|
||||
<div class="slide-content">
|
||||
<h2>${title}</h2>
|
||||
${subtitle ? `<p>${subtitle}</p>` : ''}
|
||||
<span class="btn btn-primary" data-i18n="hero_btn">立即选购</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// 无链接时,不可点击
|
||||
return `
|
||||
<div class="slide ${index === 0 ? 'active' : ''}">
|
||||
<img src="${image}" alt="${title}" loading="${index === 0 ? 'eager' : 'lazy'}">
|
||||
<div class="slide-content">
|
||||
<h2>${title}</h2>
|
||||
${subtitle ? `<p>${subtitle}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
$('#heroSlider').html(bannersHtml);
|
||||
}
|
||||
|
||||
// 轮播图管理
|
||||
const heroSlider = {
|
||||
currentSlide: 0,
|
||||
totalSlides: 0,
|
||||
autoPlayInterval: null,
|
||||
|
||||
init() {
|
||||
this.totalSlides = $('.slide').length;
|
||||
this.createDots();
|
||||
this.bindEvents();
|
||||
this.startAutoPlay();
|
||||
},
|
||||
|
||||
createDots() {
|
||||
const dotsHtml = Array.from({ length: this.totalSlides }, (_, i) =>
|
||||
`<span class="dot ${i === 0 ? 'active' : ''}" data-slide="${i}"></span>`
|
||||
).join('');
|
||||
$('.slider-dots').html(dotsHtml);
|
||||
},
|
||||
|
||||
bindEvents() {
|
||||
// 上一张
|
||||
$('.slider-btn.prev').on('click', () => {
|
||||
this.prevSlide();
|
||||
});
|
||||
|
||||
// 下一张
|
||||
$('.slider-btn.next').on('click', () => {
|
||||
this.nextSlide();
|
||||
});
|
||||
|
||||
// 点击圆点
|
||||
$(document).on('click', '.dot', (e) => {
|
||||
const slideIndex = parseInt($(e.target).data('slide'));
|
||||
this.goToSlide(slideIndex);
|
||||
});
|
||||
|
||||
// 鼠标悬停时暂停自动播放
|
||||
$('.hero-slider').on('mouseenter', () => {
|
||||
this.stopAutoPlay();
|
||||
}).on('mouseleave', () => {
|
||||
this.startAutoPlay();
|
||||
});
|
||||
},
|
||||
|
||||
goToSlide(index) {
|
||||
$('.slide').removeClass('active');
|
||||
$('.slide').eq(index).addClass('active');
|
||||
|
||||
$('.dot').removeClass('active');
|
||||
$('.dot').eq(index).addClass('active');
|
||||
|
||||
this.currentSlide = index;
|
||||
},
|
||||
|
||||
nextSlide() {
|
||||
const nextIndex = (this.currentSlide + 1) % this.totalSlides;
|
||||
this.goToSlide(nextIndex);
|
||||
},
|
||||
|
||||
prevSlide() {
|
||||
const prevIndex = (this.currentSlide - 1 + this.totalSlides) % this.totalSlides;
|
||||
this.goToSlide(prevIndex);
|
||||
},
|
||||
|
||||
startAutoPlay() {
|
||||
this.stopAutoPlay();
|
||||
this.autoPlayInterval = setInterval(() => {
|
||||
this.nextSlide();
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
stopAutoPlay() {
|
||||
if (this.autoPlayInterval) {
|
||||
clearInterval(this.autoPlayInterval);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 畅销商品数据(与product-list.js共用)
|
||||
const DEFAULT_IMAGE = 'https://picsum.photos/400/400?random=';
|
||||
|
||||
const bestsellersData = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Christmas DIY Poke Fun',
|
||||
name_en: 'Christmas DIY Poke Fun',
|
||||
name_ja: 'クリスマスDIYポークファン',
|
||||
price: 21.99,
|
||||
originalPrice: 49.99,
|
||||
image: DEFAULT_IMAGE + 'bs1',
|
||||
rating: 0,
|
||||
reviews: 0,
|
||||
badges: ['sale'],
|
||||
discount: 57
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Creative Costume Collage Set: Sweetheart',
|
||||
name_en: 'Creative Costume Collage Set: Sweetheart',
|
||||
name_ja: 'クリエイティブコスチュームコラージュセット: スイートハート',
|
||||
price: 21.99,
|
||||
originalPrice: 29.99,
|
||||
image: DEFAULT_IMAGE + 'bs2',
|
||||
rating: 4.87,
|
||||
reviews: 23,
|
||||
badges: ['hot'],
|
||||
discount: 27
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '3-in-1 Dress Up Game Set: Princess Fantasy Makeup',
|
||||
name_en: '3-in-1 Dress Up Game Set: Princess Fantasy Makeup',
|
||||
name_ja: '3-in-1 ドレスアップゲームセット: プリンセスファンタジーメイク',
|
||||
price: 19.99,
|
||||
originalPrice: 34.99,
|
||||
image: DEFAULT_IMAGE + 'bs3',
|
||||
rating: 4.86,
|
||||
reviews: 163,
|
||||
badges: ['hot'],
|
||||
discount: 43
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Magic Christmas Tree',
|
||||
name_en: 'Magic Christmas Tree',
|
||||
name_ja: 'マジッククリスマスツリー',
|
||||
price: 11.99,
|
||||
originalPrice: 19.99,
|
||||
image: DEFAULT_IMAGE + 'bs4',
|
||||
rating: 4.8,
|
||||
reviews: 15,
|
||||
badges: ['sale'],
|
||||
discount: 41
|
||||
}
|
||||
];
|
||||
|
||||
// 渲染畅销商品(使用真实 API 请求热门商品)
|
||||
function renderBestsellers() {
|
||||
// 显示加载中状态
|
||||
$('#bestsellersGrid').html('<div class="loading-spinner">加载中...</div>');
|
||||
|
||||
// 调用后端热门商品 API
|
||||
API.get('/frontend/products/hot', { page: 1, page_size: 8 })
|
||||
.then(data => {
|
||||
console.log('热门商品 API 返回数据:', data);
|
||||
|
||||
// 支持多种数据格式
|
||||
const products = data.data?.list || data.list || [];
|
||||
|
||||
if (products.length === 0) {
|
||||
$('#bestsellersGrid').html('<div class="empty-state">暂无热门商品</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('解析到的商品:', products);
|
||||
renderProductCards(products);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载热门商品失败:', error);
|
||||
$('#bestsellersGrid').html('<div class="error-state">加载失败,请刷新页面</div>');
|
||||
Toast.error(error.message || '加载热门商品失败');
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染商品卡片
|
||||
function renderProductCards(products) {
|
||||
const productsHtml = products.map(product => {
|
||||
// 处理价格:分 → 元
|
||||
const minPrice = parseInt(product.minSalePrice || product.price || 0);
|
||||
const maxPrice = parseInt(product.maxSalePrice || product.price || 0);
|
||||
const minLinePrice = parseInt(product.minLinePrice || product.original_price || 0);
|
||||
const maxLinePrice = parseInt(product.maxLinePrice || product.original_price || 0);
|
||||
|
||||
const currentPrice = PriceUtils.fenToYuan(minPrice);
|
||||
const originalPrice = minLinePrice > 0 ? PriceUtils.fenToYuan(minLinePrice) : null;
|
||||
|
||||
// 计算折扣
|
||||
let discount = 0;
|
||||
if (originalPrice && originalPrice > currentPrice) {
|
||||
discount = Math.round(((originalPrice - currentPrice) / originalPrice) * 100);
|
||||
}
|
||||
|
||||
// 商品图片
|
||||
const productImage = product.primaryImage || product.main_image || product.image || 'https://picsum.photos/400/400?random=' + (product.spuId || product.id);
|
||||
|
||||
// 商品名称
|
||||
const productName = product.title || product.name || '未知商品';
|
||||
|
||||
// 商品ID
|
||||
const productId = product.spuId || product.id;
|
||||
|
||||
// 库存
|
||||
const stock = product.spuStockQuantity || product.stock || 0;
|
||||
|
||||
// 徽章
|
||||
let badges = '';
|
||||
if (stock <= 0) {
|
||||
badges += '<span class="badge badge-danger">售罄</span>';
|
||||
} else if (stock < 10) {
|
||||
badges += '<span class="badge badge-warning">仅剩' + stock + '件</span>';
|
||||
}
|
||||
if (discount > 30) {
|
||||
badges += '<span class="badge badge-success">特惠</span>';
|
||||
}
|
||||
// 根据销量添加热门徽章
|
||||
const soldNum = product.soldNum || 0;
|
||||
if (soldNum > 100) {
|
||||
badges += '<span class="badge badge-warning">热门</span>';
|
||||
}
|
||||
|
||||
// 评分和评价数(当前 API 没有返回,暂不显示)
|
||||
const rating = product.rating || 0;
|
||||
const reviewCount = product.comment_count || 0;
|
||||
const ratingHtml = reviewCount > 0 ? `
|
||||
<div class="product-rating">
|
||||
<span class="stars">${'★'.repeat(Math.round(rating))}${'☆'.repeat(5 - Math.round(rating))}</span>
|
||||
<span class="review-count">${reviewCount} ${i18n.t('reviews')}</span>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
return `
|
||||
<div class="product-card" data-product-id="${productId}">
|
||||
<div class="product-image">
|
||||
<img src="${productImage}" alt="${productName}" loading="lazy">
|
||||
<div class="product-badges">
|
||||
${badges}
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<h3 class="product-title">${productName}</h3>
|
||||
${ratingHtml}
|
||||
<div class="product-price">
|
||||
<span class="price-current">¥${currentPrice.toFixed(2)}</span>
|
||||
${originalPrice ? `<span class="price-original">¥${originalPrice.toFixed(2)}</span>` : ''}
|
||||
${discount > 0 ? `<span class="price-discount">${i18n.t('save')} ${discount}%</span>` : ''}
|
||||
</div>
|
||||
<div class="product-actions">
|
||||
<button class="btn btn-primary add-to-cart" data-product-id="${productId}" ${stock <= 0 ? 'disabled' : ''}>
|
||||
${stock <= 0 ? '已售罄' : i18n.t('add_to_cart')}
|
||||
</button>
|
||||
<button class="btn btn-quick-view quick-view" data-product-id="${productId}">
|
||||
${i18n.t('quick_view')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
$('#bestsellersGrid').html(productsHtml);
|
||||
|
||||
// 绑定事件
|
||||
bindProductEvents();
|
||||
}
|
||||
|
||||
// 渲染用户评价
|
||||
function renderReviews() {
|
||||
// 加载高分评价
|
||||
loadHighRatingReviews();
|
||||
}
|
||||
|
||||
// 加载高分评价(直接调用专门的评论API)
|
||||
function loadHighRatingReviews() {
|
||||
console.log('=== 开始加载高分评价 ===');
|
||||
|
||||
// 直接调用高分评论API,一次请求获取所有数据
|
||||
API.get('/comments/high-rating', { limit: 6 })
|
||||
.then(data => {
|
||||
const comments = data.data || data || [];
|
||||
console.log('获取到高分评论:', comments.length, '条');
|
||||
|
||||
if (comments.length === 0) {
|
||||
renderEmptyReviews();
|
||||
return;
|
||||
}
|
||||
|
||||
// 为每个评论添加商品名称
|
||||
const reviews = comments.map(comment => ({
|
||||
...comment,
|
||||
productName: comment.product?.name || comment.product?.title || '商品'
|
||||
}));
|
||||
|
||||
renderReviewCards(reviews);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载高分评价失败:', error);
|
||||
renderEmptyReviews(true);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染评论卡片
|
||||
function renderReviewCards(reviews) {
|
||||
const reviewsHtml = reviews.map(review => {
|
||||
// 字段映射
|
||||
const rating = review.rating || 5;
|
||||
const content = review.content || '';
|
||||
const userName = review.user?.nickname || review.user?.name || '匿名用户';
|
||||
const productName = review.productName || '商品';
|
||||
const createdAt = review.created_at || review.createdAt || '';
|
||||
const images = review.images || [];
|
||||
|
||||
// 格式化日期
|
||||
let dateStr = '';
|
||||
if (createdAt) {
|
||||
const date = new Date(createdAt);
|
||||
dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 生成星星
|
||||
const stars = '★'.repeat(rating) + '☆'.repeat(5 - rating);
|
||||
|
||||
return `
|
||||
<div class="review-card">
|
||||
<div class="review-rating">${stars}</div>
|
||||
<p class="review-text">${content}</p>
|
||||
${images.length > 0 ? `
|
||||
<div class="review-images">
|
||||
${images.slice(0, 3).map(img => `
|
||||
<img src="${img}" alt="评论图片" loading="lazy">
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
<p class="review-product">商品:${productName}</p>
|
||||
<div class="review-author">
|
||||
<span class="review-author-name">${userName}</span>
|
||||
${dateStr ? `<span class="review-date">${dateStr}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
$('#reviewsGrid').html(reviewsHtml);
|
||||
}
|
||||
|
||||
// 渲染评论空状态
|
||||
function renderEmptyReviews(isError = false) {
|
||||
const lang = i18n.currentLang;
|
||||
const emptyTitle = isError ?
|
||||
(lang === 'en-US' ? 'Failed to load reviews' : lang === 'ja-JP' ? 'レビューの読み込みに失敗しました' : '加载评论失败') :
|
||||
(lang === 'en-US' ? 'No reviews yet' : lang === 'ja-JP' ? 'まだレビューはありません' : '暂无评价');
|
||||
const emptyDesc = isError ?
|
||||
(lang === 'en-US' ? 'Please try again later' : lang === 'ja-JP' ? '後でもう一度お試しください' : '请稍后再试') :
|
||||
(lang === 'en-US' ? 'Be the first to share your thoughts about our products!' : lang === 'ja-JP' ? '最初に製品についてのご意見をお聞かせください!' : '成为第一个分享购物体验的用户吧!');
|
||||
const btnText = lang === 'en-US' ? 'Shop Now' : lang === 'ja-JP' ? '今すぐショッピング' : '开始购物';
|
||||
|
||||
$('#reviewsGrid').html(`
|
||||
<div class="empty-reviews-state">
|
||||
<div class="empty-icon">
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="60" cy="60" r="50" fill="#F5F5F5"/>
|
||||
${isError ? `
|
||||
<path d="M45 45 L75 75 M75 45 L45 75" stroke="#FF6B6B" stroke-width="4" stroke-linecap="round"/>
|
||||
` : `
|
||||
<path d="M60 35 L65 50 L80 52 L70 62 L73 77 L60 70 L47 77 L50 62 L40 52 L55 50 Z" fill="#FFD93D" stroke="#FFB800" stroke-width="2"/>
|
||||
<circle cx="35" cy="45" r="3" fill="#FFD93D"/>
|
||||
<circle cx="85" cy="45" r="3" fill="#FFD93D"/>
|
||||
<circle cx="40" cy="75" r="2" fill="#FFD93D"/>
|
||||
<circle cx="80" cy="75" r="2" fill="#FFD93D"/>
|
||||
`}
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="empty-title">${emptyTitle}</h3>
|
||||
<p class="empty-desc">${emptyDesc}</p>
|
||||
${!isError ? `<a href="home.html" class="btn btn-primary empty-btn">${btnText}</a>` : ''}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// 绑定商品事件
|
||||
function bindProductEvents() {
|
||||
// 加入购物车
|
||||
$('.add-to-cart').off('click').on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const productId = parseInt($(this).data('product-id'));
|
||||
|
||||
console.log('添加商品到购物车:', productId);
|
||||
|
||||
// 先获取商品详情,确定默认SKU
|
||||
API.get(`/products/${productId}`)
|
||||
.then(productData => {
|
||||
const product = productData.data || productData;
|
||||
console.log('商品详情:', product);
|
||||
|
||||
// 构建购物车请求数据
|
||||
const cartData = {
|
||||
product_id: productId,
|
||||
quantity: 1
|
||||
};
|
||||
|
||||
// 如果商品有 SKU,选择第一个可用的 SKU
|
||||
if (product.skus && product.skus.length > 0) {
|
||||
// 查找第一个有库存的 SKU
|
||||
const availableSku = product.skus.find(sku => (sku.stock || 0) > 0);
|
||||
if (availableSku) {
|
||||
cartData.sku_id = parseInt(availableSku.id || availableSku.sku_id);
|
||||
console.log('选择 SKU:', cartData.sku_id);
|
||||
} else {
|
||||
// 如果没有库存,使用第一个 SKU
|
||||
cartData.sku_id = parseInt(product.skus[0].id || product.skus[0].sku_id);
|
||||
console.log('无库存,使用第一个 SKU:', cartData.sku_id);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('添加到购物车的数据:', cartData);
|
||||
|
||||
// 使用 API 添加到购物车
|
||||
return API.post('/cart', cartData);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('=== 添加到购物车成功 ===');
|
||||
|
||||
// 显示成功提示
|
||||
let message = '已添加到购物车';
|
||||
if (typeof i18n !== 'undefined' && i18n.t) {
|
||||
const translated = i18n.t('product_added_to_cart');
|
||||
if (translated && translated !== 'product_added_to_cart') {
|
||||
message = translated;
|
||||
}
|
||||
}
|
||||
console.log('显示 Toast 提示:', message);
|
||||
Toast.success(message);
|
||||
|
||||
// 更新购物车计数
|
||||
console.log('更新购物车计数...');
|
||||
console.log('window.cart 存在:', !!window.cart);
|
||||
console.log('cart 存在:', typeof cart !== 'undefined');
|
||||
|
||||
if (window.cart && typeof window.cart.loadCart === 'function') {
|
||||
console.log('调用 window.cart.loadCart()');
|
||||
window.cart.loadCart();
|
||||
} else if (typeof cart !== 'undefined' && typeof cart.loadCart === 'function') {
|
||||
console.log('调用 cart.loadCart()');
|
||||
cart.loadCart();
|
||||
} else {
|
||||
console.error('cart.loadCart 方法不存在');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('添加到购物车失败:', error);
|
||||
Toast.error(error.message || '添加失败');
|
||||
});
|
||||
});
|
||||
|
||||
// 快速查看
|
||||
$('.quick-view').off('click').on('click', function(e) {
|
||||
e.stopPropagation();
|
||||
const productId = $(this).data('product-id');
|
||||
window.location.href = `product-detail.html?id=${productId}`;
|
||||
});
|
||||
|
||||
// 点击卡片跳转
|
||||
$('.product-card').off('click').on('click', function() {
|
||||
const productId = $(this).data('product-id');
|
||||
window.location.href = `product-detail.html?id=${productId}`;
|
||||
});
|
||||
}
|
||||
|
||||
// 页面初始化
|
||||
$(document).ready(function() {
|
||||
// 加载轮播图(使用真实 API)
|
||||
loadBanners();
|
||||
|
||||
// 渲染畅销商品
|
||||
renderBestsellers();
|
||||
|
||||
// 渲染用户评价
|
||||
renderReviews();
|
||||
|
||||
// 监听语言切换
|
||||
$(document).on('languageChanged', function() {
|
||||
renderBestsellers();
|
||||
});
|
||||
});
|
||||
1166
web/assets/js/i18n.js
Normal file
1166
web/assets/js/i18n.js
Normal file
File diff suppressed because it is too large
Load Diff
773
web/assets/js/live-float.js
Normal file
773
web/assets/js/live-float.js
Normal file
@@ -0,0 +1,773 @@
|
||||
// Live Float Button and Modal JavaScript
|
||||
|
||||
let currentLiveStream = null; // 当前直播源数据
|
||||
|
||||
$(document).ready(function() {
|
||||
initLiveFloat();
|
||||
});
|
||||
|
||||
let liveStatsInterval = null;
|
||||
let randomMessageInterval = null;
|
||||
let floatViewersInterval = null;
|
||||
let danmakuInterval = null;
|
||||
|
||||
function initLiveFloat() {
|
||||
// 先加载直播源数据
|
||||
loadLiveStreamData();
|
||||
|
||||
bindLiveFloatEvents();
|
||||
|
||||
// 监听语言切换
|
||||
$(document).on('languageChanged', function() {
|
||||
if ($('#liveModal').hasClass('active')) {
|
||||
loadChatMessages();
|
||||
loadLiveProducts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载直播源数据
|
||||
function loadLiveStreamData() {
|
||||
LiveStreamAPI.getActiveLiveStreams()
|
||||
.then(data => {
|
||||
if (data && data.length > 0) {
|
||||
// 获取第一个有stream_url的直播源
|
||||
currentLiveStream = data.find(stream => stream.stream_url);
|
||||
|
||||
if (currentLiveStream) {
|
||||
// 有直播源,显示浮窗并更新数据
|
||||
updateFloatButton(currentLiveStream);
|
||||
$('#liveFloatBtn').show();
|
||||
|
||||
// 启动悬浮窗观看人数动态更新
|
||||
updateFloatViewers();
|
||||
floatViewersInterval = setInterval(updateFloatViewers, 3000);
|
||||
} else {
|
||||
// 没有可用的直播源,显示未开播
|
||||
showOfflineFloat();
|
||||
}
|
||||
} else {
|
||||
// 没有直播源数据,显示未开播
|
||||
showOfflineFloat();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载直播源失败:', error);
|
||||
// 加载失败,隐藏浮窗
|
||||
$('#liveFloatBtn').hide();
|
||||
});
|
||||
}
|
||||
|
||||
// 更新浮窗按钮数据
|
||||
function updateFloatButton(stream) {
|
||||
// 更新标题
|
||||
$('.live-float-title').text(stream.title);
|
||||
|
||||
// 更新平台名称
|
||||
$('.live-float-name').text(stream.platform + '官方直播');
|
||||
|
||||
// 如果有封面图,更新封面
|
||||
if (stream.cover_image) {
|
||||
const $video = $('.live-float-video');
|
||||
$video.replaceWith(`<img src="${stream.cover_image}" alt="${stream.title}" class="live-float-video" style="width: 100%; height: 100%; object-fit: cover;">`);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示未开播状态
|
||||
function showOfflineFloat() {
|
||||
$('#liveFloatBtn').show().addClass('offline');
|
||||
|
||||
// 移除LIVE徽章
|
||||
$('.live-float-badge-top').html(`
|
||||
<span class="offline-badge-float">未开播</span>
|
||||
`);
|
||||
|
||||
// 更新标题
|
||||
$('.live-float-title').text('暂未开播');
|
||||
$('.live-float-name').text('敬请期待');
|
||||
$('.live-float-desc').text('主播休息中...');
|
||||
|
||||
// 清除定时器
|
||||
if (floatViewersInterval) {
|
||||
clearInterval(floatViewersInterval);
|
||||
floatViewersInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
function bindLiveFloatEvents() {
|
||||
// 点击悬浮按钮打开直播
|
||||
$('#liveFloatBtn').on('click', function() {
|
||||
if (currentLiveStream && currentLiveStream.stream_url) {
|
||||
// 有直播源,打开直播弹窗
|
||||
openLiveModal();
|
||||
} else {
|
||||
// 未开播,显示提示
|
||||
if (typeof Toast !== 'undefined') {
|
||||
Toast.show('直播暂未开始,敬请期待', 'info');
|
||||
} else {
|
||||
alert('直播暂未开始,敬请期待');
|
||||
}
|
||||
}
|
||||
});
|
||||
// 点击关闭按钮
|
||||
$('#liveModalClose').on('click', function() {
|
||||
closeLiveModal();
|
||||
});
|
||||
|
||||
// 点击模态框背景关闭
|
||||
$('#liveModal').on('click', function(e) {
|
||||
if (e.target.id === 'liveModal') {
|
||||
closeLiveModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Tab切换
|
||||
$('.tab-btn').on('click', function() {
|
||||
const tabName = $(this).data('tab');
|
||||
switchTab(tabName);
|
||||
});
|
||||
|
||||
// 点赞按钮
|
||||
$('#likeBtn').on('click', function() {
|
||||
handleLike();
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
$('#sendMessageBtn').on('click', function() {
|
||||
sendMessage();
|
||||
});
|
||||
|
||||
$('#chatInput').on('keypress', function(e) {
|
||||
if (e.which === 13) {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 产品点击
|
||||
$(document).on('click', '.live-product-item', function() {
|
||||
const productId = $(this).data('product-id');
|
||||
// 关闭直播窗口并跳转到商品详情
|
||||
closeLiveModal();
|
||||
setTimeout(() => {
|
||||
window.location.href = `product-detail.html?id=${productId}`;
|
||||
}, 300);
|
||||
});
|
||||
|
||||
$(document).on('click', '.live-product-btn', function(e) {
|
||||
e.stopPropagation();
|
||||
const productId = $(this).closest('.live-product-item').data('product-id');
|
||||
addProductToCart(productId);
|
||||
});
|
||||
}
|
||||
|
||||
// 打开直播模态框
|
||||
function openLiveModal() {
|
||||
if (!currentLiveStream || !currentLiveStream.stream_url) {
|
||||
if (typeof Toast !== 'undefined') {
|
||||
Toast.show('直播暂未开始,敬请期待', 'info');
|
||||
} else {
|
||||
alert('直播暂未开始,敬请期待');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$('#liveModal').addClass('active');
|
||||
$('body').css('overflow', 'hidden');
|
||||
|
||||
// 更新主播信息
|
||||
$('#liveHostName').text(currentLiveStream.title);
|
||||
$('#liveViewerCount').text(formatViewCount(currentLiveStream.view_count || 1234));
|
||||
|
||||
// 显示并绑定跳转按钮
|
||||
const $gotoBtn = $('#gotoPlatformBtnTop');
|
||||
$('#platformNameTop').text(currentLiveStream.platform);
|
||||
$gotoBtn.show().off('click').on('click', function() {
|
||||
window.open(currentLiveStream.stream_url, '_blank');
|
||||
});
|
||||
|
||||
// 更新视频源
|
||||
const videoContainer = $('.live-video-container');
|
||||
videoContainer.html(`
|
||||
<iframe
|
||||
src="${currentLiveStream.stream_url}"
|
||||
class="live-video-player"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
allow="autoplay; fullscreen; picture-in-picture"
|
||||
scrolling="no"
|
||||
style="width: 100%; height: 100%; position: absolute; top: 0; left: 0; pointer-events: auto;">
|
||||
</iframe>
|
||||
<div class="live-top-bar">
|
||||
<div class="live-host-info">
|
||||
<div class="host-avatar">
|
||||
<img src="https://picsum.photos/40/40?random=host" alt="主播">
|
||||
</div>
|
||||
<div class="host-details">
|
||||
<div class="host-name">${currentLiveStream.title}</div>
|
||||
<div class="host-viewers">
|
||||
<span>${formatViewCount(currentLiveStream.view_count || 1234)}</span> 人在线
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-follow">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
关注
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn-goto-platform-top" onclick="window.open('${currentLiveStream.stream_url}', '_blank')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||
<polyline points="15 3 21 3 21 9"></polyline>
|
||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||
</svg>
|
||||
前往${currentLiveStream.platform}观看
|
||||
</button>
|
||||
</div>
|
||||
<div class="live-interaction-bar">
|
||||
<button class="interaction-btn" id="likeBtn" title="点赞">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
|
||||
</svg>
|
||||
<span id="likeCount">1.2W</span>
|
||||
</button>
|
||||
<button class="interaction-btn" title="评论">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
<span id="commentCount">8923</span>
|
||||
</button>
|
||||
<button class="interaction-btn" title="分享">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="18" cy="5" r="3"></circle>
|
||||
<circle cx="6" cy="12" r="3"></circle>
|
||||
<circle cx="18" cy="19" r="3"></circle>
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
|
||||
</svg>
|
||||
<span>分享</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="danmaku-container" id="danmakuContainer"></div>
|
||||
<div class="live-chat-preview" id="chatPreview"></div>
|
||||
`);
|
||||
|
||||
// 绑定点赞按钮
|
||||
$('#likeBtn').on('click', function() {
|
||||
handleLike();
|
||||
});
|
||||
|
||||
// 增加观看次数
|
||||
LiveStreamAPI.incrementViewCount(currentLiveStream.id)
|
||||
.then(() => {
|
||||
console.log('观看次数已增加');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('增加观看次数失败:', error);
|
||||
});
|
||||
|
||||
// 加载内容
|
||||
loadChatMessages();
|
||||
loadLiveProducts();
|
||||
|
||||
// 开始定时更新
|
||||
randomMessageInterval = setInterval(addRandomMessage, 10000);
|
||||
danmakuInterval = setInterval(addRandomDanmaku, 3000);
|
||||
}
|
||||
|
||||
// 格式化观看人数
|
||||
function formatViewCount(count) {
|
||||
if (!count || count === 0) return '0';
|
||||
if (count < 1000) return count.toString();
|
||||
if (count < 10000) return (count / 1000).toFixed(1) + 'K';
|
||||
return (count / 10000).toFixed(1) + 'W';
|
||||
}
|
||||
|
||||
// 关闭直播模态框
|
||||
function closeLiveModal() {
|
||||
$('#liveModal').removeClass('active');
|
||||
$('body').css('overflow', '');
|
||||
|
||||
// 恢复原始视频容器结构(移除controls属性)
|
||||
const videoContainer = $('.live-video-container');
|
||||
videoContainer.html(`
|
||||
<video id="liveVideo" class="live-video-player" autoplay muted loop playsinline>
|
||||
<source src="https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/720/Big_Buck_Bunny_720_10s_1MB.mp4" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div class="live-indicator">
|
||||
<span class="live-dot"></span>
|
||||
<span data-i18n="live_status">🔴 直播中</span>
|
||||
</div>
|
||||
<div class="danmaku-container" id="danmakuContainer"></div>
|
||||
`);
|
||||
|
||||
// 清除定时器
|
||||
if (liveStatsInterval) {
|
||||
clearInterval(liveStatsInterval);
|
||||
liveStatsInterval = null;
|
||||
}
|
||||
if (randomMessageInterval) {
|
||||
clearInterval(randomMessageInterval);
|
||||
randomMessageInterval = null;
|
||||
}
|
||||
if (danmakuInterval) {
|
||||
clearInterval(danmakuInterval);
|
||||
danmakuInterval = null;
|
||||
}
|
||||
|
||||
// 清除弹幕
|
||||
$('#danmakuContainer').empty();
|
||||
}
|
||||
|
||||
// Tab切换
|
||||
function switchTab(tabName) {
|
||||
$('.tab-btn').removeClass('active');
|
||||
$(`.tab-btn[data-tab="${tabName}"]`).addClass('active');
|
||||
|
||||
$('.tab-content').removeClass('active');
|
||||
$(`#${tabName}Tab`).addClass('active');
|
||||
}
|
||||
|
||||
// 加载聊天消息
|
||||
function loadChatMessages() {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
const messages = [
|
||||
{
|
||||
type: 'system',
|
||||
text: lang === 'zh-CN' ? '欢迎来到直播间!' :
|
||||
lang === 'en-US' ? 'Welcome to the live stream!' :
|
||||
'ライブ配信へようこそ!',
|
||||
time: '10:00'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '小明' : lang === 'en-US' ? 'Mike' : 'タロウ',
|
||||
text: lang === 'zh-CN' ? '这款拼图看起来很不错!' :
|
||||
lang === 'en-US' ? 'This puzzle looks great!' :
|
||||
'このパズルは素晴らしいですね!',
|
||||
time: '10:05'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '小红' : lang === 'en-US' ? 'Lucy' : 'ハナコ',
|
||||
text: lang === 'zh-CN' ? '适合多大年龄的孩子?' :
|
||||
lang === 'en-US' ? 'What age is it suitable for?' :
|
||||
'何歳の子供に適していますか?',
|
||||
time: '10:06'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '主播' : lang === 'en-US' ? 'Host' : 'ホスト',
|
||||
text: lang === 'zh-CN' ? '这款拼图适合3-6岁的孩子,可以培养动手能力和空间想象力!' :
|
||||
lang === 'en-US' ? 'This puzzle is suitable for children aged 3-6 and helps develop hands-on skills and spatial imagination!' :
|
||||
'このパズルは3〜6歳の子供に適しており、実践的なスキルと空間想像力を養うのに役立ちます!',
|
||||
time: '10:07'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '李华' : lang === 'en-US' ? 'David' : 'ダビデ',
|
||||
text: lang === 'zh-CN' ? '价格很优惠,已经下单了!' :
|
||||
lang === 'en-US' ? 'Great price, just ordered!' :
|
||||
'価格もお得で、注文しました!',
|
||||
time: '10:08'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '王芳' : lang === 'en-US' ? 'Sarah' : 'サラ',
|
||||
text: lang === 'zh-CN' ? '材质安全吗?' :
|
||||
lang === 'en-US' ? 'Is the material safe?' :
|
||||
'素材は安全ですか?',
|
||||
time: '10:09'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '主播' : lang === 'en-US' ? 'Host' : 'ホスト',
|
||||
text: lang === 'zh-CN' ? '所有产品都通过了安全认证,使用环保材料,家长可以放心!' :
|
||||
lang === 'en-US' ? 'All products are safety certified and made from eco-friendly materials. Parents can rest assured!' :
|
||||
'すべての製品は安全認証を受けており、環境に優しい素材を使用しています。親御さんも安心です!',
|
||||
time: '10:10'
|
||||
}
|
||||
];
|
||||
|
||||
let chatHtml = '';
|
||||
messages.forEach(msg => {
|
||||
if (msg.type === 'system') {
|
||||
chatHtml += `
|
||||
<div class="chat-message system">
|
||||
<div class="chat-text">${msg.text}</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
chatHtml += `
|
||||
<div class="chat-message">
|
||||
<div class="chat-message-header">
|
||||
<span class="chat-username">${msg.username}</span>
|
||||
<span class="chat-time">${msg.time}</span>
|
||||
</div>
|
||||
<div class="chat-text">${msg.text}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
$('#chatMessages').html(chatHtml);
|
||||
scrollChatToBottom();
|
||||
}
|
||||
|
||||
// 加载直播商品
|
||||
function loadLiveProducts() {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
const products = [
|
||||
{
|
||||
id: 1,
|
||||
name: '200片拼图 - 海洋世界',
|
||||
name_en: '200 Piece Puzzle - Ocean World',
|
||||
name_ja: '200ピースパズル - 海の世界',
|
||||
price: 19.99,
|
||||
originalPrice: 29.99,
|
||||
image: 'https://picsum.photos/200/200?random=live1'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '艺术绘画套装',
|
||||
name_en: 'Art Painting Set',
|
||||
name_ja: 'アート絵画セット',
|
||||
price: 24.99,
|
||||
originalPrice: 34.99,
|
||||
image: 'https://picsum.photos/200/200?random=live2'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '木质积木 - 100块',
|
||||
name_en: 'Wooden Blocks - 100 Pieces',
|
||||
name_ja: '木製ブロック - 100ピース',
|
||||
price: 29.99,
|
||||
originalPrice: 39.99,
|
||||
image: 'https://picsum.photos/200/200?random=live3'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '磁力片建构玩具',
|
||||
name_en: 'Magnetic Building Tiles',
|
||||
name_ja: 'マグネットタイル',
|
||||
price: 34.99,
|
||||
originalPrice: 49.99,
|
||||
image: 'https://picsum.photos/200/200?random=live4'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '儿童桌游套装',
|
||||
name_en: 'Kids Board Game Set',
|
||||
name_ja: '子供用ボードゲームセット',
|
||||
price: 27.99,
|
||||
originalPrice: 37.99,
|
||||
image: 'https://picsum.photos/200/200?random=live5'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '手工贴纸书',
|
||||
name_en: 'Sticker Activity Book',
|
||||
name_ja: 'ステッカーブック',
|
||||
price: 12.99,
|
||||
originalPrice: 18.99,
|
||||
image: 'https://picsum.photos/200/200?random=live6'
|
||||
}
|
||||
];
|
||||
|
||||
let productsHtml = '';
|
||||
products.forEach(product => {
|
||||
const productName = lang === 'zh-CN' ? product.name :
|
||||
lang === 'en-US' ? product.name_en :
|
||||
product.name_ja;
|
||||
const btnText = i18n.t('add_to_cart');
|
||||
|
||||
productsHtml += `
|
||||
<div class="live-product-item" data-product-id="${product.id}">
|
||||
<div class="live-product-image">
|
||||
<img src="${product.image}" alt="${productName}">
|
||||
</div>
|
||||
<div class="live-product-info">
|
||||
<div class="live-product-title">${productName}</div>
|
||||
<div class="live-product-price">
|
||||
<span class="live-product-current-price">$${product.price.toFixed(2)}</span>
|
||||
<span class="live-product-original-price">$${product.originalPrice.toFixed(2)}</span>
|
||||
</div>
|
||||
<button class="live-product-btn">${btnText}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#liveProducts').html(productsHtml);
|
||||
}
|
||||
|
||||
// 更新直播统计
|
||||
function updateLiveStats() {
|
||||
const currentViewers = parseInt($('#viewerCount').text().replace(/,/g, ''));
|
||||
const currentLikes = parseInt($('#likeCount').text().replace(/,/g, ''));
|
||||
const currentMessages = parseInt($('#messageCount').text().replace(/,/g, ''));
|
||||
|
||||
// 随机增加数值
|
||||
const newViewers = currentViewers + Math.floor(Math.random() * 20) - 5;
|
||||
const newLikes = currentLikes + Math.floor(Math.random() * 15);
|
||||
const newMessages = currentMessages + Math.floor(Math.random() * 5);
|
||||
|
||||
$('#viewerCount').text(Math.max(1000, newViewers).toLocaleString());
|
||||
$('#likeCount').text(Math.max(500, newLikes).toLocaleString());
|
||||
$('#messageCount').text(Math.max(50, newMessages).toLocaleString());
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
function sendMessage() {
|
||||
const message = $('#chatInput').val().trim();
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lang = i18n.currentLang;
|
||||
const username = lang === 'zh-CN' ? '我' : lang === 'en-US' ? 'Me' : '私';
|
||||
const currentTime = new Date();
|
||||
const timeString = `${currentTime.getHours()}:${currentTime.getMinutes().toString().padStart(2, '0')}`;
|
||||
|
||||
// 添加到聊天记录
|
||||
const messageHtml = `
|
||||
<div class="chat-message">
|
||||
<div class="chat-message-header">
|
||||
<span class="chat-username">${username}</span>
|
||||
<span class="chat-time">${timeString}</span>
|
||||
</div>
|
||||
<div class="chat-text">${message}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#chatMessages').append(messageHtml);
|
||||
|
||||
// 如果开启了弹幕,也显示在弹幕区
|
||||
if ($('#danmakuToggle').is(':checked')) {
|
||||
createDanmaku(message);
|
||||
}
|
||||
|
||||
$('#chatInput').val('');
|
||||
scrollChatToBottom();
|
||||
|
||||
// 更新消息计数
|
||||
const currentCount = parseInt($('#messageCount').text().replace(/,/g, ''));
|
||||
$('#messageCount').text((currentCount + 1).toLocaleString());
|
||||
}
|
||||
|
||||
// 添加随机消息
|
||||
function addRandomMessage() {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
const usernames = lang === 'zh-CN' ? ['张三', '李四', '王五', '赵六'] :
|
||||
lang === 'en-US' ? ['John', 'Jane', 'Bob', 'Alice'] :
|
||||
['太郎', '花子', '次郎', '美咲'];
|
||||
|
||||
const messages = lang === 'zh-CN' ?
|
||||
['这个产品真不错!', '价格很实惠', '已经下单了', '质量怎么样?', '有优惠码吗?', '主播讲得很详细', '这个颜色好看'] :
|
||||
lang === 'en-US' ?
|
||||
['This product is great!', 'Great price', 'Just ordered', 'How\'s the quality?', 'Any discount codes?', 'Very informative', 'Love this color'] :
|
||||
['この製品は素晴らしいです!', '価格もお得', '注文しました', '品質はどうですか?', '割引コードはありますか?', 'とても詳しい', 'この色いいね'];
|
||||
|
||||
const randomUsername = usernames[Math.floor(Math.random() * usernames.length)];
|
||||
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
|
||||
const currentTime = new Date();
|
||||
const timeString = `${currentTime.getHours()}:${currentTime.getMinutes().toString().padStart(2, '0')}`;
|
||||
|
||||
const messageHtml = `
|
||||
<div class="chat-message">
|
||||
<div class="chat-message-header">
|
||||
<span class="chat-username">${randomUsername}</span>
|
||||
<span class="chat-time">${timeString}</span>
|
||||
</div>
|
||||
<div class="chat-text">${randomMessage}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#chatMessages').append(messageHtml);
|
||||
scrollChatToBottom();
|
||||
|
||||
// 更新消息计数
|
||||
const currentCount = parseInt($('#messageCount').text().replace(/,/g, ''));
|
||||
$('#messageCount').text((currentCount + 1).toLocaleString());
|
||||
}
|
||||
|
||||
// 滚动聊天到底部
|
||||
function scrollChatToBottom() {
|
||||
const chatMessages = document.getElementById('chatMessages');
|
||||
if (chatMessages) {
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新悬浮窗观看人数
|
||||
function updateFloatViewers() {
|
||||
const lang = i18n.currentLang;
|
||||
const viewers = ['1.8W', '1.9W', '2.0W', '2.1W', '1.7W'];
|
||||
const randomViewers = viewers[Math.floor(Math.random() * viewers.length)];
|
||||
|
||||
const viewersText = lang === 'zh-CN' ? `${randomViewers}观看` :
|
||||
lang === 'en-US' ? `${randomViewers} watching` :
|
||||
`${randomViewers}視聴中`;
|
||||
|
||||
$('#floatViewers').text(viewersText);
|
||||
}
|
||||
|
||||
// 处理点赞
|
||||
function handleLike() {
|
||||
const $likeBtn = $('#likeBtn');
|
||||
$likeBtn.addClass('liked');
|
||||
|
||||
// 更新点赞数
|
||||
const currentLikes = parseInt($('#likeCount').text().replace(/,/g, ''));
|
||||
$('#likeCount').text((currentLikes + 1).toLocaleString());
|
||||
|
||||
// 移除动画类
|
||||
setTimeout(() => {
|
||||
$likeBtn.removeClass('liked');
|
||||
}, 500);
|
||||
|
||||
// 生成点赞动画
|
||||
createLikeAnimation();
|
||||
}
|
||||
|
||||
// 创建点赞动画
|
||||
function createLikeAnimation() {
|
||||
const $container = $('.live-video-container');
|
||||
const colors = ['#ff6b6b', '#ff8787', '#ffa5a5', '#ff5252', '#ee5a6f'];
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
setTimeout(() => {
|
||||
const $heart = $('<div class="like-animation">❤️</div>');
|
||||
$heart.css({
|
||||
position: 'absolute',
|
||||
bottom: '20%',
|
||||
right: Math.random() * 30 + 5 + '%',
|
||||
fontSize: Math.random() * 15 + 25 + 'px',
|
||||
color: colors[Math.floor(Math.random() * colors.length)],
|
||||
animation: 'likeFloat 2.5s ease-out forwards',
|
||||
zIndex: 100,
|
||||
pointerEvents: 'none'
|
||||
});
|
||||
|
||||
$container.append($heart);
|
||||
|
||||
setTimeout(() => {
|
||||
$heart.remove();
|
||||
}, 2500);
|
||||
}, i * 200);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加随机弹幕
|
||||
function addRandomDanmaku() {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
const messages = lang === 'zh-CN' ?
|
||||
['这个好看!', '已经下单了', '主播讲得很好', '价格优惠', '喜欢这款', '质量怎么样?', '太棒了!', '有优惠吗?'] :
|
||||
lang === 'en-US' ?
|
||||
['Love this!', 'Just ordered', 'Great demo', 'Good price', 'Like this one', 'How\'s the quality?', 'Amazing!', 'Any discounts?'] :
|
||||
['これいい!', '注文しました', '素晴らしい', '価格が良い', '好きです', '品質は?', '最高!', '割引は?'];
|
||||
|
||||
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
|
||||
|
||||
createDanmaku(randomMessage);
|
||||
}
|
||||
|
||||
// 创建弹幕
|
||||
function createDanmaku(text) {
|
||||
const $container = $('#danmakuContainer');
|
||||
const containerWidth = $container.width();
|
||||
const containerHeight = $container.height();
|
||||
|
||||
if (!containerWidth || !containerHeight) return;
|
||||
|
||||
const $danmaku = $('<div class="danmaku-item"></div>');
|
||||
$danmaku.text(text);
|
||||
|
||||
// 随机高度位置
|
||||
const top = Math.random() * (containerHeight - 50);
|
||||
const duration = 6 + Math.random() * 2; // 6-8秒,更快
|
||||
|
||||
$danmaku.css({
|
||||
top: top + 'px',
|
||||
left: containerWidth + 'px',
|
||||
animationDuration: duration + 's'
|
||||
});
|
||||
|
||||
$container.append($danmaku);
|
||||
|
||||
// 弹幕结束后移除
|
||||
setTimeout(() => {
|
||||
$danmaku.remove();
|
||||
}, duration * 1000);
|
||||
}
|
||||
|
||||
// 添加商品到购物车
|
||||
function addProductToCart(productId) {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
// 获取产品信息
|
||||
const products = [
|
||||
{
|
||||
id: 1,
|
||||
name: '200片拼图 - 海洋世界',
|
||||
name_en: '200 Piece Puzzle - Ocean World',
|
||||
name_ja: '200ピースパズル - 海の世界',
|
||||
price: 19.99,
|
||||
image: 'https://picsum.photos/200/200?random=live1'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '艺术绘画套装',
|
||||
name_en: 'Art Painting Set',
|
||||
name_ja: 'アート絵画セット',
|
||||
price: 24.99,
|
||||
image: 'https://picsum.photos/200/200?random=live2'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '木质积木 - 100块',
|
||||
name_en: 'Wooden Blocks - 100 Pieces',
|
||||
name_ja: '木製ブロック - 100ピース',
|
||||
price: 29.99,
|
||||
image: 'https://picsum.photos/200/200?random=live3'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '磁力片建构玩具',
|
||||
name_en: 'Magnetic Building Tiles',
|
||||
name_ja: 'マグネットタイル',
|
||||
price: 34.99,
|
||||
image: 'https://picsum.photos/200/200?random=live4'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '儿童桌游套装',
|
||||
name_en: 'Kids Board Game Set',
|
||||
name_ja: '子供用ボードゲームセット',
|
||||
price: 27.99,
|
||||
image: 'https://picsum.photos/200/200?random=live5'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '手工贴纸书',
|
||||
name_en: 'Sticker Activity Book',
|
||||
name_ja: 'ステッカーブック',
|
||||
price: 12.99,
|
||||
image: 'https://picsum.photos/200/200?random=live6'
|
||||
}
|
||||
];
|
||||
|
||||
const product = products.find(p => p.id === productId);
|
||||
if (product) {
|
||||
cart.addToCart(product);
|
||||
|
||||
const message = i18n.t('product_added_to_cart') ||
|
||||
(lang === 'zh-CN' ? '商品已添加到购物车' :
|
||||
lang === 'en-US' ? 'Product added to cart' :
|
||||
'商品がカートに追加されました');
|
||||
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
341
web/assets/js/live.js
Normal file
341
web/assets/js/live.js
Normal file
@@ -0,0 +1,341 @@
|
||||
// Live Stream Page JavaScript
|
||||
|
||||
$(document).ready(function() {
|
||||
initLivePage();
|
||||
});
|
||||
|
||||
function initLivePage() {
|
||||
loadChatMessages();
|
||||
loadLiveProducts();
|
||||
updateLiveStats();
|
||||
bindLiveEvents();
|
||||
|
||||
// 监听语言切换
|
||||
$(document).on('languageChanged', function() {
|
||||
loadChatMessages();
|
||||
loadLiveProducts();
|
||||
});
|
||||
|
||||
// 自动滚动到最新消息
|
||||
scrollChatToBottom();
|
||||
|
||||
// 模拟实时更新统计数据
|
||||
setInterval(updateLiveStats, 5000);
|
||||
|
||||
// 模拟新消息
|
||||
setInterval(addRandomMessage, 10000);
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
function bindLiveEvents() {
|
||||
// Tab切换
|
||||
$('.tab-btn').on('click', function() {
|
||||
const tabName = $(this).data('tab');
|
||||
switchTab(tabName);
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
$('#sendMessageBtn').on('click', function() {
|
||||
sendMessage();
|
||||
});
|
||||
|
||||
$('#chatInput').on('keypress', function(e) {
|
||||
if (e.which === 13) {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 产品点击
|
||||
$(document).on('click', '.live-product-item', function() {
|
||||
const productId = $(this).data('product-id');
|
||||
window.location.href = `product-detail.html?id=${productId}`;
|
||||
});
|
||||
|
||||
$(document).on('click', '.live-product-btn', function(e) {
|
||||
e.stopPropagation();
|
||||
const productId = $(this).closest('.live-product-item').data('product-id');
|
||||
addProductToCart(productId);
|
||||
});
|
||||
}
|
||||
|
||||
// Tab切换
|
||||
function switchTab(tabName) {
|
||||
$('.tab-btn').removeClass('active');
|
||||
$(`.tab-btn[data-tab="${tabName}"]`).addClass('active');
|
||||
|
||||
$('.tab-content').removeClass('active');
|
||||
$(`#${tabName}Tab`).addClass('active');
|
||||
}
|
||||
|
||||
// 加载聊天消息
|
||||
function loadChatMessages() {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
const messages = [
|
||||
{
|
||||
type: 'system',
|
||||
text: lang === 'zh-CN' ? '欢迎来到直播间!' :
|
||||
lang === 'en-US' ? 'Welcome to the live stream!' :
|
||||
'ライブ配信へようこそ!',
|
||||
time: '10:00'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '小明' : lang === 'en-US' ? 'Mike' : 'タロウ',
|
||||
text: lang === 'zh-CN' ? '这款拼图看起来很不错!' :
|
||||
lang === 'en-US' ? 'This puzzle looks great!' :
|
||||
'このパズルは素晴らしいですね!',
|
||||
time: '10:05'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '小红' : lang === 'en-US' ? 'Lucy' : 'ハナコ',
|
||||
text: lang === 'zh-CN' ? '适合多大年龄的孩子?' :
|
||||
lang === 'en-US' ? 'What age is it suitable for?' :
|
||||
'何歳の子供に適していますか?',
|
||||
time: '10:06'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '主播' : lang === 'en-US' ? 'Host' : 'ホスト',
|
||||
text: lang === 'zh-CN' ? '这款拼图适合3-6岁的孩子,可以培养动手能力和空间想象力!' :
|
||||
lang === 'en-US' ? 'This puzzle is suitable for children aged 3-6 and helps develop hands-on skills and spatial imagination!' :
|
||||
'このパズルは3〜6歳の子供に適しており、実践的なスキルと空間想像力を養うのに役立ちます!',
|
||||
time: '10:07'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '李华' : lang === 'en-US' ? 'David' : 'ダビデ',
|
||||
text: lang === 'zh-CN' ? '价格很优惠,已经下单了!' :
|
||||
lang === 'en-US' ? 'Great price, just ordered!' :
|
||||
'価格もお得で、注文しました!',
|
||||
time: '10:08'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '王芳' : lang === 'en-US' ? 'Sarah' : 'サラ',
|
||||
text: lang === 'zh-CN' ? '材质安全吗?' :
|
||||
lang === 'en-US' ? 'Is the material safe?' :
|
||||
'素材は安全ですか?',
|
||||
time: '10:09'
|
||||
},
|
||||
{
|
||||
username: lang === 'zh-CN' ? '主播' : lang === 'en-US' ? 'Host' : 'ホスト',
|
||||
text: lang === 'zh-CN' ? '所有产品都通过了安全认证,使用环保材料,家长可以放心!' :
|
||||
lang === 'en-US' ? 'All products are safety certified and made from eco-friendly materials. Parents can rest assured!' :
|
||||
'すべての製品は安全認証を受けており、環境に優しい素材を使用しています。親御さんも安心です!',
|
||||
time: '10:10'
|
||||
}
|
||||
];
|
||||
|
||||
let chatHtml = '';
|
||||
messages.forEach(msg => {
|
||||
if (msg.type === 'system') {
|
||||
chatHtml += `
|
||||
<div class="chat-message system">
|
||||
<div class="chat-text">${msg.text}</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
chatHtml += `
|
||||
<div class="chat-message">
|
||||
<div class="chat-message-header">
|
||||
<span class="chat-username">${msg.username}</span>
|
||||
<span class="chat-time">${msg.time}</span>
|
||||
</div>
|
||||
<div class="chat-text">${msg.text}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
$('#chatMessages').html(chatHtml);
|
||||
scrollChatToBottom();
|
||||
}
|
||||
|
||||
// 加载直播商品
|
||||
function loadLiveProducts() {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
const products = [
|
||||
{
|
||||
id: 1,
|
||||
name: '200片拼图 - 海洋世界',
|
||||
name_en: '200 Piece Puzzle - Ocean World',
|
||||
name_ja: '200ピースパズル - 海の世界',
|
||||
price: 19.99,
|
||||
originalPrice: 29.99,
|
||||
image: 'https://picsum.photos/200/200?random=live1'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '艺术绘画套装',
|
||||
name_en: 'Art Painting Set',
|
||||
name_ja: 'アート絵画セット',
|
||||
price: 24.99,
|
||||
originalPrice: 34.99,
|
||||
image: 'https://picsum.photos/200/200?random=live2'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '木质积木 - 100块',
|
||||
name_en: 'Wooden Blocks - 100 Pieces',
|
||||
name_ja: '木製ブロック - 100ピース',
|
||||
price: 29.99,
|
||||
originalPrice: 39.99,
|
||||
image: 'https://picsum.photos/200/200?random=live3'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '磁力片建构玩具',
|
||||
name_en: 'Magnetic Building Tiles',
|
||||
name_ja: 'マグネットタイル',
|
||||
price: 34.99,
|
||||
originalPrice: 49.99,
|
||||
image: 'https://picsum.photos/200/200?random=live4'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '儿童桌游套装',
|
||||
name_en: 'Kids Board Game Set',
|
||||
name_ja: '子供用ボードゲームセット',
|
||||
price: 27.99,
|
||||
originalPrice: 37.99,
|
||||
image: 'https://picsum.photos/200/200?random=live5'
|
||||
}
|
||||
];
|
||||
|
||||
let productsHtml = '';
|
||||
products.forEach(product => {
|
||||
const productName = lang === 'zh-CN' ? product.name :
|
||||
lang === 'en-US' ? product.name_en :
|
||||
product.name_ja;
|
||||
const btnText = i18n.t('add_to_cart');
|
||||
|
||||
productsHtml += `
|
||||
<div class="live-product-item" data-product-id="${product.id}">
|
||||
<div class="live-product-image">
|
||||
<img src="${product.image}" alt="${productName}">
|
||||
</div>
|
||||
<div class="live-product-info">
|
||||
<div class="live-product-title">${productName}</div>
|
||||
<div class="live-product-price">
|
||||
<span class="live-product-current-price">$${product.price.toFixed(2)}</span>
|
||||
<span class="live-product-original-price">$${product.originalPrice.toFixed(2)}</span>
|
||||
</div>
|
||||
<button class="live-product-btn">${btnText}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#liveProducts').html(productsHtml);
|
||||
}
|
||||
|
||||
// 更新直播统计
|
||||
function updateLiveStats() {
|
||||
const currentViewers = parseInt($('#viewerCount').text().replace(/,/g, ''));
|
||||
const currentLikes = parseInt($('#likeCount').text().replace(/,/g, ''));
|
||||
const currentMessages = parseInt($('#messageCount').text().replace(/,/g, ''));
|
||||
|
||||
// 随机增加数值
|
||||
const newViewers = currentViewers + Math.floor(Math.random() * 20) - 5;
|
||||
const newLikes = currentLikes + Math.floor(Math.random() * 15);
|
||||
const newMessages = currentMessages + Math.floor(Math.random() * 5);
|
||||
|
||||
$('#viewerCount').text(Math.max(1000, newViewers).toLocaleString());
|
||||
$('#likeCount').text(Math.max(500, newLikes).toLocaleString());
|
||||
$('#messageCount').text(Math.max(50, newMessages).toLocaleString());
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
function sendMessage() {
|
||||
const message = $('#chatInput').val().trim();
|
||||
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lang = i18n.currentLang;
|
||||
const username = lang === 'zh-CN' ? '我' : lang === 'en-US' ? 'Me' : '私';
|
||||
const currentTime = new Date();
|
||||
const timeString = `${currentTime.getHours()}:${currentTime.getMinutes().toString().padStart(2, '0')}`;
|
||||
|
||||
const messageHtml = `
|
||||
<div class="chat-message">
|
||||
<div class="chat-message-header">
|
||||
<span class="chat-username">${username}</span>
|
||||
<span class="chat-time">${timeString}</span>
|
||||
</div>
|
||||
<div class="chat-text">${message}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#chatMessages').append(messageHtml);
|
||||
$('#chatInput').val('');
|
||||
scrollChatToBottom();
|
||||
|
||||
// 更新消息计数
|
||||
const currentCount = parseInt($('#messageCount').text().replace(/,/g, ''));
|
||||
$('#messageCount').text((currentCount + 1).toLocaleString());
|
||||
}
|
||||
|
||||
// 添加随机消息
|
||||
function addRandomMessage() {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
const usernames = lang === 'zh-CN' ? ['张三', '李四', '王五', '赵六'] :
|
||||
lang === 'en-US' ? ['John', 'Jane', 'Bob', 'Alice'] :
|
||||
['太郎', '花子', '次郎', '美咲'];
|
||||
|
||||
const messages = lang === 'zh-CN' ?
|
||||
['这个产品真不错!', '价格很实惠', '已经下单了', '质量怎么样?', '有优惠码吗?'] :
|
||||
lang === 'en-US' ?
|
||||
['This product is great!', 'Great price', 'Just ordered', 'How\'s the quality?', 'Any discount codes?'] :
|
||||
['この製品は素晴らしいです!', '価格もお得', '注文しました', '品質はどうですか?', '割引コードはありますか?'];
|
||||
|
||||
const randomUsername = usernames[Math.floor(Math.random() * usernames.length)];
|
||||
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
|
||||
const currentTime = new Date();
|
||||
const timeString = `${currentTime.getHours()}:${currentTime.getMinutes().toString().padStart(2, '0')}`;
|
||||
|
||||
const messageHtml = `
|
||||
<div class="chat-message">
|
||||
<div class="chat-message-header">
|
||||
<span class="chat-username">${randomUsername}</span>
|
||||
<span class="chat-time">${timeString}</span>
|
||||
</div>
|
||||
<div class="chat-text">${randomMessage}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#chatMessages').append(messageHtml);
|
||||
scrollChatToBottom();
|
||||
}
|
||||
|
||||
// 滚动聊天到底部
|
||||
function scrollChatToBottom() {
|
||||
const chatMessages = document.getElementById('chatMessages');
|
||||
if (chatMessages) {
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加商品到购物车
|
||||
function addProductToCart(productId) {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
// 这里使用模拟数据,实际应该从产品列表中获取
|
||||
const product = {
|
||||
id: productId,
|
||||
name: '直播商品',
|
||||
name_en: 'Live Product',
|
||||
name_ja: 'ライブ商品',
|
||||
price: 19.99,
|
||||
image: 'https://picsum.photos/200/200?random=' + productId
|
||||
};
|
||||
|
||||
cart.addToCart(product);
|
||||
|
||||
const message = i18n.t('product_added_to_cart') ||
|
||||
(lang === 'zh-CN' ? '商品已添加到购物车' :
|
||||
lang === 'en-US' ? 'Product added to cart' :
|
||||
'商品がカートに追加されました');
|
||||
|
||||
alert(message);
|
||||
}
|
||||
160
web/assets/js/livestream.js
Normal file
160
web/assets/js/livestream.js
Normal file
@@ -0,0 +1,160 @@
|
||||
// 直播投流源管理
|
||||
const LiveStreamManager = {
|
||||
// 初始化
|
||||
init: function() {
|
||||
this.loadLiveStreams();
|
||||
},
|
||||
|
||||
// 加载直播投流源
|
||||
loadLiveStreams: function() {
|
||||
LiveStreamAPI.getActiveLiveStreams()
|
||||
.then(data => {
|
||||
if (data && data.length > 0) {
|
||||
this.renderLiveStreams(data);
|
||||
$('#livestreamSection').show();
|
||||
} else {
|
||||
$('#livestreamSection').hide();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载直播投流源失败:', error);
|
||||
$('#livestreamSection').hide();
|
||||
});
|
||||
},
|
||||
|
||||
// 渲染直播投流源列表
|
||||
renderLiveStreams: function(streams) {
|
||||
const grid = $('#livestreamGrid');
|
||||
grid.empty();
|
||||
|
||||
streams.forEach(stream => {
|
||||
const card = this.createStreamCard(stream);
|
||||
grid.append(card);
|
||||
});
|
||||
},
|
||||
|
||||
// 创建直播卡片
|
||||
createStreamCard: function(stream) {
|
||||
const platformColors = {
|
||||
'抖音': '#000',
|
||||
'快手': '#ff6600',
|
||||
'淘宝': '#ff4400',
|
||||
'京东': '#e60012',
|
||||
'小红书': '#ff2442',
|
||||
'视频号': '#07c160'
|
||||
};
|
||||
|
||||
const platformColor = platformColors[stream.platform] || '#666';
|
||||
const isLive = !!stream.stream_url; // 判断是否有直播源
|
||||
|
||||
const card = $('<div class="livestream-card"></div>');
|
||||
|
||||
// 如果未开播,添加未开播样式类
|
||||
if (!isLive) {
|
||||
card.addClass('not-live');
|
||||
}
|
||||
|
||||
// 封面图片
|
||||
if (stream.cover_image) {
|
||||
card.append(`
|
||||
<img src="${stream.cover_image}"
|
||||
alt="${stream.title}"
|
||||
class="cover-image"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex'">
|
||||
<div class="placeholder-cover" style="display: none;">
|
||||
📺
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
card.append(`
|
||||
<div class="placeholder-cover">
|
||||
📺
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// 直播标识或未开播标识
|
||||
if (isLive) {
|
||||
card.append('<div class="live-badge">LIVE</div>');
|
||||
} else {
|
||||
card.append('<div class="offline-badge">未开播</div>');
|
||||
}
|
||||
|
||||
// 平台标识
|
||||
card.append(`
|
||||
<div class="platform-badge" style="background-color: ${platformColor};">
|
||||
${stream.platform}
|
||||
</div>
|
||||
`);
|
||||
|
||||
// 卡片内容
|
||||
const btnText = isLive ? '观看直播' : '暂未开播';
|
||||
const btnClass = isLive ? 'watch-btn' : 'watch-btn disabled';
|
||||
|
||||
const content = $(`
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">${stream.title}</h3>
|
||||
<p class="card-description">${stream.description || (isLive ? '欢迎来到直播间' : '主播暂时不在,敬请期待')}</p>
|
||||
<div class="card-footer">
|
||||
<div class="view-count">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
<span>${this.formatViewCount(stream.view_count)}</span>
|
||||
</div>
|
||||
<button class="${btnClass}">${btnText}</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
card.append(content);
|
||||
|
||||
// 只有开播时才添加点击事件
|
||||
if (isLive) {
|
||||
card.on('click', () => {
|
||||
this.openLiveStream(stream);
|
||||
});
|
||||
} else {
|
||||
// 未开播时点击提示
|
||||
card.on('click', () => {
|
||||
Toast.show('该平台暂未开播,敬请期待', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
return card;
|
||||
},
|
||||
|
||||
// 格式化观看次数
|
||||
formatViewCount: function(count) {
|
||||
if (!count || count === 0) return '0';
|
||||
if (count < 1000) return count.toString();
|
||||
if (count < 10000) return (count / 1000).toFixed(1) + 'K';
|
||||
if (count < 100000) return (count / 10000).toFixed(1) + 'W';
|
||||
return (count / 10000).toFixed(0) + 'W';
|
||||
},
|
||||
|
||||
// 打开直播投流源
|
||||
openLiveStream: function(stream) {
|
||||
// 增加观看次数
|
||||
LiveStreamAPI.incrementViewCount(stream.id)
|
||||
.then(() => {
|
||||
console.log('观看次数已增加');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('增加观看次数失败:', error);
|
||||
});
|
||||
|
||||
// 在新窗口打开直播URL
|
||||
if (stream.stream_url) {
|
||||
window.open(stream.stream_url, '_blank');
|
||||
} else {
|
||||
Toast.show('直播地址无效', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载完成后初始化
|
||||
$(document).ready(function() {
|
||||
LiveStreamManager.init();
|
||||
});
|
||||
208
web/assets/js/login.js
Normal file
208
web/assets/js/login.js
Normal file
@@ -0,0 +1,208 @@
|
||||
// Login Page JavaScript
|
||||
|
||||
$(document).ready(function() {
|
||||
initLoginPage();
|
||||
initTestAccount();
|
||||
});
|
||||
|
||||
function initLoginPage() {
|
||||
bindLoginEvents();
|
||||
checkLoginStatus();
|
||||
}
|
||||
|
||||
// 初始化测试账号
|
||||
function initTestAccount() {
|
||||
const users = JSON.parse(localStorage.getItem('users') || '[]');
|
||||
|
||||
// 检查是否已存在测试账号
|
||||
const testUser = users.find(u => u.email === 'test@vizee.com');
|
||||
|
||||
if (!testUser) {
|
||||
// 创建测试账号
|
||||
const defaultTestUser = {
|
||||
firstName: '测试',
|
||||
lastName: '用户',
|
||||
email: 'test@vizee.com',
|
||||
password: '123456',
|
||||
registerTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
users.push(defaultTestUser);
|
||||
localStorage.setItem('users', JSON.stringify(users));
|
||||
|
||||
console.log('测试账号已创建:test@vizee.com / 123456');
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
function bindLoginEvents() {
|
||||
// 切换到注册表单
|
||||
$('#showRegister').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('#loginForm').hide();
|
||||
$('#registerForm').show();
|
||||
});
|
||||
|
||||
// 切换到登录表单
|
||||
$('#showLogin').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('#registerForm').hide();
|
||||
$('#loginForm').show();
|
||||
});
|
||||
|
||||
// 登录表单提交
|
||||
$('#loginForm form').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
handleLogin();
|
||||
});
|
||||
|
||||
// 注册表单提交
|
||||
$('#registerForm form').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
handleRegister();
|
||||
});
|
||||
|
||||
// 社交登录
|
||||
$('.btn-google').on('click', function() {
|
||||
Toast.info(i18n.t('feature_coming_soon') || '功能开发中...');
|
||||
});
|
||||
|
||||
$('.btn-wechat').on('click', function() {
|
||||
Toast.info(i18n.t('feature_coming_soon') || '功能开发中...');
|
||||
});
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
function checkLoginStatus() {
|
||||
const user = localStorage.getItem('currentUser');
|
||||
if (user) {
|
||||
// 已登录,检查是否有重定向URL
|
||||
const redirectUrl = localStorage.getItem('redirectUrl');
|
||||
if (redirectUrl) {
|
||||
localStorage.removeItem('redirectUrl');
|
||||
window.location.href = redirectUrl;
|
||||
} else {
|
||||
// 跳转到用户中心
|
||||
window.location.href = 'user-center.html';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理登录
|
||||
function handleLogin() {
|
||||
const email = $('#loginEmail').val().trim();
|
||||
const password = $('#loginPassword').val().trim();
|
||||
|
||||
if (!email || !password) {
|
||||
Toast.warning(i18n.t('please_fill_all_fields') || '请填写所有字段');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
Toast.error(i18n.t('invalid_email') || '请输入有效的邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
const $btn = $('#loginForm button[type="submit"]');
|
||||
const originalText = $btn.text();
|
||||
$btn.prop('disabled', true).text(i18n.t('logging_in') || '登录中...');
|
||||
|
||||
// 调用后端登录API
|
||||
UserAPI.login(email, password)
|
||||
.then(function(data) {
|
||||
// 登录成功 - 保存后端返回的完整用户信息
|
||||
const currentUser = {
|
||||
...data.user, // 展开后端返回的完整用户对象(包括phone, avatar, gender等)
|
||||
token: data.token,
|
||||
loginTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
localStorage.setItem('currentUser', JSON.stringify(currentUser));
|
||||
|
||||
// 更新用户图标显示
|
||||
if (typeof window.updateUserIcon === 'function') {
|
||||
window.updateUserIcon();
|
||||
}
|
||||
|
||||
Toast.success(i18n.t('login_success') || '登录成功!');
|
||||
|
||||
// 检查是否有重定向URL
|
||||
setTimeout(() => {
|
||||
const redirectUrl = localStorage.getItem('redirectUrl');
|
||||
if (redirectUrl) {
|
||||
localStorage.removeItem('redirectUrl');
|
||||
window.location.href = redirectUrl;
|
||||
} else {
|
||||
window.location.href = 'user-center.html';
|
||||
}
|
||||
}, 500);
|
||||
})
|
||||
.catch(function(error) {
|
||||
// 登录失败
|
||||
console.error('登录失败:', error);
|
||||
Toast.error(error.message || i18n.t('login_failed') || '登录失败,请检查邮箱和密码');
|
||||
$btn.prop('disabled', false).text(originalText);
|
||||
});
|
||||
}
|
||||
|
||||
// 处理注册
|
||||
function handleRegister() {
|
||||
const firstName = $('#registerFirstName').val().trim();
|
||||
const lastName = $('#registerLastName').val().trim();
|
||||
const email = $('#registerEmail').val().trim();
|
||||
const password = $('#registerPassword').val().trim();
|
||||
const confirmPassword = $('#registerConfirmPassword').val().trim();
|
||||
|
||||
if (!firstName || !lastName || !email || !password || !confirmPassword) {
|
||||
Toast.warning(i18n.t('please_fill_all_fields') || '请填写所有字段');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
Toast.error(i18n.t('invalid_email') || '请输入有效的邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
Toast.error(i18n.t('password_too_short') || '密码长度至少6个字符');
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
Toast.error(i18n.t('password_not_match') || '两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
const $btn = $('#registerForm button[type="submit"]');
|
||||
const originalText = $btn.text();
|
||||
$btn.prop('disabled', true).text(i18n.t('registering') || '注册中...');
|
||||
|
||||
// 调用后端注册API
|
||||
const nickname = firstName + ' ' + lastName;
|
||||
UserAPI.register(email, password, nickname)
|
||||
.then(function(data) {
|
||||
// 注册成功
|
||||
Toast.success(i18n.t('register_success') || '注册成功!请登录');
|
||||
|
||||
// 切换到登录表单
|
||||
$('#registerForm').hide();
|
||||
$('#loginForm').show();
|
||||
$('#loginEmail').val(email);
|
||||
|
||||
$btn.prop('disabled', false).text(originalText);
|
||||
})
|
||||
.catch(function(error) {
|
||||
// 注册失败
|
||||
console.error('注册失败:', error);
|
||||
Toast.error(error.message || i18n.t('register_failed') || '注册失败,请稍后重试');
|
||||
$btn.prop('disabled', false).text(originalText);
|
||||
});
|
||||
}
|
||||
|
||||
// 验证邮箱格式
|
||||
function isValidEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
757
web/assets/js/product-detail.js
Normal file
757
web/assets/js/product-detail.js
Normal file
@@ -0,0 +1,757 @@
|
||||
// 商品详情页功能 - 使用真实API
|
||||
|
||||
let currentProduct = null;
|
||||
let selectedSku = null;
|
||||
let currentCommentPage = 1;
|
||||
let currentCommentFilter = 'all'; // all, good, medium, bad, image
|
||||
|
||||
// 页面初始化
|
||||
$(document).ready(function() {
|
||||
const productId = getProductIdFromUrl();
|
||||
if (productId) {
|
||||
loadProductDetail(productId);
|
||||
loadCommentsCount(productId);
|
||||
loadComments(productId);
|
||||
} else {
|
||||
Toast.error('商品不存在');
|
||||
setTimeout(() => {
|
||||
window.location.href = 'index.html';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
bindEvents();
|
||||
loadCartCount();
|
||||
});
|
||||
|
||||
// 从URL获取商品ID
|
||||
function getProductIdFromUrl() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get('id');
|
||||
}
|
||||
|
||||
// 加载商品详情
|
||||
function loadProductDetail(productId) {
|
||||
console.log('加载商品详情:', productId);
|
||||
|
||||
API.get(`/frontend/products/${productId}/detail`)
|
||||
.then(data => {
|
||||
console.log('商品详情数据:', data);
|
||||
currentProduct = data;
|
||||
renderProductDetail(data);
|
||||
loadRelatedProducts();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载商品详情失败:', error);
|
||||
Toast.error(error.message || '加载商品详情失败');
|
||||
setTimeout(() => {
|
||||
window.location.href = 'index.html';
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染商品详情
|
||||
function renderProductDetail(product) {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
// 设置商品名称
|
||||
const productName = product.title || product.name || '未知商品';
|
||||
$('#productName').text(productName);
|
||||
$('#breadcrumbProduct').text(productName);
|
||||
document.title = `${productName} - vizee为之甄选`;
|
||||
|
||||
// 设置价格
|
||||
const minPrice = PriceUtils.fenToYuan(product.minSalePrice || product.price || 0);
|
||||
const maxPrice = PriceUtils.fenToYuan(product.maxSalePrice || product.price || 0);
|
||||
const originPrice = PriceUtils.fenToYuan(product.maxLinePrice || product.originPrice || 0);
|
||||
|
||||
if (minPrice === maxPrice) {
|
||||
$('#currentPrice').text(`¥${minPrice.toFixed(2)}`);
|
||||
} else {
|
||||
$('#currentPrice').text(`¥${minPrice.toFixed(2)} - ¥${maxPrice.toFixed(2)}`);
|
||||
}
|
||||
|
||||
if (originPrice > maxPrice) {
|
||||
$('#originalPrice').text(`¥${originPrice.toFixed(2)}`).show();
|
||||
const discount = Math.round((1 - maxPrice / originPrice) * 100);
|
||||
$('#priceSave').text(`${i18n.t('save') || '省'} ${discount}%`).show();
|
||||
} else {
|
||||
$('#originalPrice').hide();
|
||||
$('#priceSave').hide();
|
||||
}
|
||||
|
||||
// 设置库存状态
|
||||
const spuStock = product.spuStockQuantity ?? product.stock ?? 0;
|
||||
const isInStock = spuStock > 0;
|
||||
|
||||
if (isInStock) {
|
||||
$('#stockStatus').text('有货').addClass('in-stock').removeClass('out-of-stock');
|
||||
} else {
|
||||
$('#stockStatus').text('缺货').addClass('out-of-stock').removeClass('in-stock');
|
||||
}
|
||||
|
||||
// 保存库存状态到currentProduct
|
||||
currentProduct.isStock = isInStock;
|
||||
|
||||
// 设置图片
|
||||
loadProductImages(product.images || [product.primaryImage]);
|
||||
|
||||
// 设置描述
|
||||
const description = product.descriptionText || product.details || '';
|
||||
$('#productDescription').text(description);
|
||||
|
||||
// 渲染详情图片
|
||||
if (product.desc && Array.isArray(product.desc) && product.desc.length > 0) {
|
||||
const detailImagesHtml = product.desc.map(img =>
|
||||
`<img src="${img}" alt="商品详情" loading="lazy">`
|
||||
).join('');
|
||||
$('#productDetailImages').html(detailImagesHtml);
|
||||
}
|
||||
|
||||
// 渲染规格选择器
|
||||
if (product.specList && product.specList.length > 0) {
|
||||
renderSpecSelector(product.specList, product.skuList);
|
||||
} else {
|
||||
$('#specSelector').hide();
|
||||
}
|
||||
|
||||
// 显示已售数量
|
||||
if (product.soldNum) {
|
||||
$('#soldCount').text(`已售 ${product.soldNum}`).show();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载商品图片
|
||||
function loadProductImages(images) {
|
||||
// 显示加载动画
|
||||
showImageLoading();
|
||||
|
||||
if (!images || images.length === 0) {
|
||||
// 如果没有图片,隐藏加载动画并显示默认图片
|
||||
hideImageLoading();
|
||||
images = ['https://picsum.photos/800/800?random=default'];
|
||||
}
|
||||
|
||||
// 主图
|
||||
const mainImg = new Image();
|
||||
mainImg.onload = function() {
|
||||
$('#mainImage').attr('src', images[0]);
|
||||
hideImageLoading();
|
||||
};
|
||||
mainImg.onerror = function() {
|
||||
// 加载失败,使用占位图
|
||||
$('#mainImage').attr('src', 'https://picsum.photos/800/800?random=error');
|
||||
hideImageLoading();
|
||||
};
|
||||
mainImg.src = images[0];
|
||||
|
||||
// 缩略图
|
||||
const thumbnailsHtml = images.map((img, index) =>
|
||||
`<div class="thumbnail ${index === 0 ? 'active' : ''}" data-index="${index}">
|
||||
<img src="${img}" alt="商品图片 ${index + 1}" loading="lazy">
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
$('#thumbnails').html(thumbnailsHtml);
|
||||
}
|
||||
|
||||
// 显示图片加载动画
|
||||
function showImageLoading() {
|
||||
const loadingHtml = `
|
||||
<div class="image-loading" id="imageLoading">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">加载中...</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if ($('#imageLoading').length === 0) {
|
||||
$('.main-image').append(loadingHtml);
|
||||
}
|
||||
$('#mainImage').css('opacity', '0');
|
||||
}
|
||||
|
||||
// 隐藏图片加载动画
|
||||
function hideImageLoading() {
|
||||
$('#imageLoading').fadeOut(300, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
$('#mainImage').css('opacity', '1');
|
||||
}
|
||||
|
||||
// 渲染规格选择器
|
||||
function renderSpecSelector(specList, skuList) {
|
||||
let specHtml = '';
|
||||
|
||||
specList.forEach((spec, specIndex) => {
|
||||
// 优先使用title,其次specName,最后name
|
||||
const specName = spec.title || spec.specName || spec.name || `规格${specIndex + 1}`;
|
||||
const specValues = spec.specValueList || [];
|
||||
|
||||
specHtml += `
|
||||
<div class="spec-group">
|
||||
<label class="spec-label">${specName}</label>
|
||||
<div class="spec-options" data-spec-index="${specIndex}">
|
||||
${specValues.map((value, valueIndex) => {
|
||||
const valueName = value.specValue || value.name || value;
|
||||
return `
|
||||
<button class="spec-option"
|
||||
data-spec-index="${specIndex}"
|
||||
data-value-index="${valueIndex}"
|
||||
data-spec-value="${valueName}">
|
||||
${valueName}
|
||||
</button>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
$('#specSelector').html(specHtml).show();
|
||||
}
|
||||
|
||||
// 加载相关商品
|
||||
function loadRelatedProducts() {
|
||||
API.get('/products', { page: 1, page_size: 4 })
|
||||
.then(data => {
|
||||
const products = data.list || [];
|
||||
renderRelatedProducts(products.slice(0, 4));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载相关商品失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染相关商品
|
||||
function renderRelatedProducts(products) {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
const productsHtml = products.map(product => {
|
||||
const productName = product.name || '未知商品';
|
||||
const productImage = product.main_image || 'https://picsum.photos/400/400?random=default';
|
||||
const price = PriceUtils.fenToYuan(product.price || 0);
|
||||
const originalPrice = product.orig_price ? PriceUtils.fenToYuan(product.orig_price) : null;
|
||||
|
||||
return `
|
||||
<div class="product-card">
|
||||
<a href="product-detail.html?id=${product.id}">
|
||||
<div class="product-image">
|
||||
<img src="${productImage}" alt="${productName}" loading="lazy">
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<h3 class="product-title">${productName}</h3>
|
||||
<div class="product-price">
|
||||
<span class="price-current">¥${price.toFixed(2)}</span>
|
||||
${originalPrice ? `<span class="price-original">¥${originalPrice.toFixed(2)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
$('#relatedProducts').html(productsHtml);
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
function bindEvents() {
|
||||
// 缩略图点击
|
||||
$(document).on('click', '.thumbnail', function() {
|
||||
const index = $(this).data('index');
|
||||
const imgSrc = $(this).find('img').attr('src');
|
||||
$('#mainImage').attr('src', imgSrc);
|
||||
$('.thumbnail').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
});
|
||||
|
||||
// 数量增减
|
||||
$('.qty-btn.minus').on('click', function() {
|
||||
const $input = $('#quantityInput');
|
||||
let qty = parseInt($input.val()) || 1;
|
||||
if (qty > 1) {
|
||||
$input.val(qty - 1);
|
||||
}
|
||||
});
|
||||
|
||||
$('.qty-btn.plus').on('click', function() {
|
||||
const $input = $('#quantityInput');
|
||||
let qty = parseInt($input.val()) || 1;
|
||||
const max = parseInt($input.attr('max')) || 99;
|
||||
if (qty < max) {
|
||||
$input.val(qty + 1);
|
||||
}
|
||||
});
|
||||
|
||||
$('#quantityInput').on('change', function() {
|
||||
let qty = parseInt($(this).val()) || 1;
|
||||
const max = parseInt($(this).attr('max')) || 99;
|
||||
if (qty < 1) qty = 1;
|
||||
if (qty > max) qty = max;
|
||||
$(this).val(qty);
|
||||
});
|
||||
|
||||
// 规格选择
|
||||
$(document).on('click', '.spec-option', function() {
|
||||
const $option = $(this);
|
||||
const specIndex = $option.data('spec-index');
|
||||
|
||||
// 取消同组其他选项
|
||||
$(`.spec-option[data-spec-index="${specIndex}"]`).removeClass('selected');
|
||||
// 选中当前选项
|
||||
$option.addClass('selected');
|
||||
|
||||
// 更新选中的SKU
|
||||
updateSelectedSku();
|
||||
});
|
||||
|
||||
// 评论标签切换
|
||||
$(document).on('click', '.comment-tab', function() {
|
||||
const $tab = $(this);
|
||||
const filter = $tab.data('filter');
|
||||
|
||||
$('.comment-tab').removeClass('active');
|
||||
$tab.addClass('active');
|
||||
|
||||
currentCommentFilter = filter;
|
||||
currentCommentPage = 1;
|
||||
|
||||
const productId = getProductIdFromUrl();
|
||||
loadComments(productId, 1, filter);
|
||||
});
|
||||
|
||||
// 评论分页
|
||||
$(document).on('click', '#commentPagination .page-btn', function() {
|
||||
const page = parseInt($(this).data('page'));
|
||||
const productId = getProductIdFromUrl();
|
||||
|
||||
currentCommentPage = page;
|
||||
loadComments(productId, page, currentCommentFilter);
|
||||
|
||||
// 滚动到评论区域
|
||||
$('html, body').animate({
|
||||
scrollTop: $('#reviews').offset().top - 100
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Tab切换
|
||||
$(document).on('click', '.tab-header', function() {
|
||||
const $tab = $(this);
|
||||
const tabId = $tab.data('tab');
|
||||
|
||||
$('.tab-header').removeClass('active');
|
||||
$tab.addClass('active');
|
||||
|
||||
$('.tab-content').removeClass('active');
|
||||
$(`#${tabId}`).addClass('active');
|
||||
});
|
||||
|
||||
// 立即购买
|
||||
$('#buyNowBtn').on('click', function() {
|
||||
if (!currentProduct || !currentProduct.isStock) {
|
||||
Toast.error('商品缺货');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果有规格,检查是否已选择
|
||||
if (currentProduct.specList && currentProduct.specList.length > 0) {
|
||||
const selectedCount = $('.spec-option.selected').length;
|
||||
if (selectedCount < currentProduct.specList.length) {
|
||||
// 找到未选择的规格组
|
||||
const unselectedSpecs = [];
|
||||
currentProduct.specList.forEach((spec, index) => {
|
||||
const hasSelected = $(`.spec-option.selected[data-spec-index="${index}"]`).length > 0;
|
||||
if (!hasSelected) {
|
||||
const specName = spec.title || spec.specName || spec.name || `规格${index + 1}`;
|
||||
unselectedSpecs.push(specName);
|
||||
// 高亮未选择的规格组
|
||||
$(`.spec-options[data-spec-index="${index}"]`).addClass('spec-required');
|
||||
setTimeout(() => {
|
||||
$(`.spec-options[data-spec-index="${index}"]`).removeClass('spec-required');
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
Toast.error(`请选择${unselectedSpecs.join('、')}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const quantity = parseInt($('#quantityInput').val()) || 1;
|
||||
|
||||
// 先加入购物车,然后跳转结算
|
||||
addToCart(quantity, true);
|
||||
});
|
||||
|
||||
// 加入购物车
|
||||
$('#addToCartBtn').on('click', function() {
|
||||
if (!currentProduct || !currentProduct.isStock) {
|
||||
Toast.error('商品缺货');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果有规格,检查是否已选择
|
||||
if (currentProduct.specList && currentProduct.specList.length > 0) {
|
||||
const selectedCount = $('.spec-option.selected').length;
|
||||
if (selectedCount < currentProduct.specList.length) {
|
||||
// 找到未选择的规格组
|
||||
const unselectedSpecs = [];
|
||||
currentProduct.specList.forEach((spec, index) => {
|
||||
const hasSelected = $(`.spec-option.selected[data-spec-index="${index}"]`).length > 0;
|
||||
if (!hasSelected) {
|
||||
const specName = spec.title || spec.specName || spec.name || `规格${index + 1}`;
|
||||
unselectedSpecs.push(specName);
|
||||
// 高亮未选择的规格组
|
||||
$(`.spec-options[data-spec-index="${index}"]`).addClass('spec-required');
|
||||
setTimeout(() => {
|
||||
$(`.spec-options[data-spec-index="${index}"]`).removeClass('spec-required');
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
Toast.error(`请选择${unselectedSpecs.join('、')}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const quantity = parseInt($('#quantityInput').val()) || 1;
|
||||
addToCart(quantity, false);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新选中的SKU
|
||||
function updateSelectedSku() {
|
||||
if (!currentProduct || !currentProduct.skuList || currentProduct.skuList.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取所有选中的规格值
|
||||
const selectedSpecs = [];
|
||||
$('.spec-option.selected').each(function() {
|
||||
selectedSpecs.push($(this).data('spec-value'));
|
||||
});
|
||||
|
||||
// 查找匹配的SKU
|
||||
const sku = currentProduct.skuList.find(s => {
|
||||
if (!s.specInfo || s.specInfo.length !== selectedSpecs.length) {
|
||||
return false;
|
||||
}
|
||||
return s.specInfo.every(spec => selectedSpecs.includes(spec.specValue));
|
||||
});
|
||||
|
||||
if (sku) {
|
||||
selectedSku = sku;
|
||||
// 更新价格显示
|
||||
const price = PriceUtils.fenToYuan(sku.priceInfo && sku.priceInfo[0] ? sku.priceInfo[0].price : 0);
|
||||
$('#currentPrice').text(`¥${price.toFixed(2)}`);
|
||||
|
||||
// 更新库存
|
||||
const stock = sku.stockQuantity || 0;
|
||||
if (stock > 0) {
|
||||
$('#stockStatus').text('有货').addClass('in-stock').removeClass('out-of-stock');
|
||||
$('#quantity').attr('max', stock);
|
||||
} else {
|
||||
$('#stockStatus').text('缺货').addClass('out-of-stock').removeClass('in-stock');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加入购物车
|
||||
function addToCart(quantity, buyNow = false) {
|
||||
// 如果是立即购买,直接跳转到结算页,不需要加入购物车
|
||||
if (buyNow) {
|
||||
const productId = currentProduct.spuId || currentProduct.id;
|
||||
const skuId = selectedSku ? (selectedSku.skuId || selectedSku.id) : '';
|
||||
|
||||
// 构建 URL 参数
|
||||
const params = new URLSearchParams({
|
||||
type: 'buynow',
|
||||
product_id: productId,
|
||||
quantity: quantity
|
||||
});
|
||||
|
||||
if (skuId) {
|
||||
params.append('sku_id', skuId);
|
||||
}
|
||||
|
||||
// 直接跳转到结算页
|
||||
window.location.href = `checkout.html?${params.toString()}`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 普通加入购物车流程
|
||||
const data = {
|
||||
product_id: parseInt(currentProduct.spuId || currentProduct.id),
|
||||
quantity: quantity
|
||||
};
|
||||
|
||||
// 如果选择了SKU
|
||||
if (selectedSku) {
|
||||
data.sku_id = parseInt(selectedSku.skuId || selectedSku.id);
|
||||
}
|
||||
|
||||
console.log('加入购物车:', data);
|
||||
|
||||
API.post('/cart', data)
|
||||
.then(() => {
|
||||
Toast.success('已添加到购物车');
|
||||
loadCartCount();
|
||||
|
||||
// 打开购物车抽屉
|
||||
setTimeout(() => {
|
||||
if (typeof window.openCartDrawer === 'function') {
|
||||
window.openCartDrawer();
|
||||
}
|
||||
}, 300);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('添加到购物车失败:', error);
|
||||
Toast.error(error.message || '添加失败');
|
||||
});
|
||||
}
|
||||
|
||||
// 加载购物车数量
|
||||
function loadCartCount() {
|
||||
// 检查是否登录,未登录时不请求
|
||||
const user = localStorage.getItem('currentUser');
|
||||
if (!user) {
|
||||
$('.cart-count').text(0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userData = JSON.parse(user);
|
||||
if (!userData.token) {
|
||||
$('.cart-count').text(0);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
$('.cart-count').text(0);
|
||||
return;
|
||||
}
|
||||
|
||||
API.get('/cart')
|
||||
.then(data => {
|
||||
const totalQuantity = data.total_quantity || 0;
|
||||
$('.cart-count').text(totalQuantity);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载购物车数量失败:', error);
|
||||
$('.cart-count').text(0);
|
||||
});
|
||||
}
|
||||
|
||||
// 加载评论统计
|
||||
function loadCommentsCount(productId) {
|
||||
API.get(`/comments/products/${productId}/stats`)
|
||||
.then(data => {
|
||||
const totalCount = data.total_count || 0;
|
||||
const avgRating = data.average_rating || 0;
|
||||
const goodCount = (data.rating_4_count || 0) + (data.rating_5_count || 0);
|
||||
const mediumCount = data.rating_3_count || 0;
|
||||
const badCount = (data.rating_1_count || 0) + (data.rating_2_count || 0);
|
||||
const imageCount = data.has_images_count || 0;
|
||||
|
||||
// 更新评论数量显示
|
||||
$('#reviewCount').text(`${totalCount} reviews`);
|
||||
|
||||
// 更新星级显示
|
||||
renderStars(avgRating);
|
||||
|
||||
// 更新评论标签
|
||||
updateCommentTabs(totalCount, goodCount, mediumCount, badCount, imageCount);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载评论统计失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染星级
|
||||
function renderStars(rating) {
|
||||
const fullStars = Math.floor(rating);
|
||||
const hasHalfStar = rating % 1 >= 0.5;
|
||||
let starsHtml = '';
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (i < fullStars) {
|
||||
starsHtml += '★';
|
||||
} else if (i === fullStars && hasHalfStar) {
|
||||
starsHtml += '☆';
|
||||
} else {
|
||||
starsHtml += '☆';
|
||||
}
|
||||
}
|
||||
|
||||
$('#productStars').html(starsHtml);
|
||||
}
|
||||
|
||||
// 更新评论标签
|
||||
function updateCommentTabs(total, good, medium, bad, image) {
|
||||
const tabsHtml = `
|
||||
<div class="comment-tabs">
|
||||
<button class="comment-tab active" data-filter="all">全部 (${total})</button>
|
||||
<button class="comment-tab" data-filter="good">好评 (${good})</button>
|
||||
<button class="comment-tab" data-filter="medium">中评 (${medium})</button>
|
||||
<button class="comment-tab" data-filter="bad">差评 (${bad})</button>
|
||||
<button class="comment-tab" data-filter="image">有图 (${image})</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#reviewsList').before(tabsHtml);
|
||||
}
|
||||
|
||||
// 加载评论列表
|
||||
function loadComments(productId, page = 1, filter = 'all') {
|
||||
const params = {
|
||||
page: page,
|
||||
page_size: 10
|
||||
};
|
||||
|
||||
// 根据筛选条件添加参数
|
||||
if (filter === 'good') {
|
||||
params.rating = 4; // 4-5星
|
||||
} else if (filter === 'medium') {
|
||||
params.rating = 3; // 3星
|
||||
} else if (filter === 'bad') {
|
||||
params.rating = 2; // 1-2星
|
||||
} else if (filter === 'image') {
|
||||
params.has_images = true;
|
||||
}
|
||||
|
||||
API.get(`/comments/products/${productId}`, params)
|
||||
.then(data => {
|
||||
const comments = data.list || [];
|
||||
renderComments(comments);
|
||||
|
||||
// 更新分页
|
||||
renderCommentPagination(data.page, data.total, data.page_size);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载评论失败:', error);
|
||||
$('#reviewsList').html('<div class="empty-reviews">暂无评论</div>');
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染评论列表
|
||||
function renderComments(comments) {
|
||||
if (comments.length === 0) {
|
||||
$('#reviewsList').html('<div class="empty-reviews">暂无评论</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
const commentsHtml = comments.map(comment => {
|
||||
const userName = comment.user_name || '匿名用户';
|
||||
const userAvatar = comment.user_avatar || 'https://picsum.photos/40/40?random=' + comment.id;
|
||||
const rating = comment.rating || 5;
|
||||
const content = comment.content || '';
|
||||
const images = comment.images || [];
|
||||
const createdAt = formatDate(comment.created_at);
|
||||
const productSpec = comment.product_spec || '';
|
||||
const replyContent = comment.reply_content || '';
|
||||
|
||||
// 星级
|
||||
let starsHtml = '';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
starsHtml += i < rating ? '★' : '☆';
|
||||
}
|
||||
|
||||
// 图片
|
||||
let imagesHtml = '';
|
||||
if (images.length > 0) {
|
||||
imagesHtml = `
|
||||
<div class="comment-images">
|
||||
${images.map(img => `
|
||||
<img src="${img}" alt="评论图片" class="comment-image" onclick="previewImage('${img}')">
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 商家回复
|
||||
let replyHtml = '';
|
||||
if (replyContent) {
|
||||
replyHtml = `
|
||||
<div class="merchant-reply">
|
||||
<div class="reply-label">商家回复:</div>
|
||||
<div class="reply-content">${replyContent}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="review-item">
|
||||
<div class="review-header">
|
||||
<div class="review-author-info">
|
||||
<img src="${userAvatar}" alt="${userName}" class="review-avatar">
|
||||
<div>
|
||||
<div class="review-author-name">${userName}</div>
|
||||
${productSpec ? `<div class="review-spec">${productSpec}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-meta">
|
||||
<div class="review-rating">${starsHtml}</div>
|
||||
<div class="review-date">${createdAt}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-content">
|
||||
<p class="review-text">${content}</p>
|
||||
${imagesHtml}
|
||||
</div>
|
||||
${replyHtml}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
$('#reviewsList').html(commentsHtml);
|
||||
}
|
||||
|
||||
// 渲染评论分页
|
||||
function renderCommentPagination(currentPage, total, pageSize) {
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
if (totalPages <= 1) {
|
||||
$('#commentPagination').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
let paginationHtml = `<div class="pagination">`;
|
||||
|
||||
// 上一页
|
||||
if (currentPage > 1) {
|
||||
paginationHtml += `<button class="page-btn" data-page="${currentPage - 1}">上一页</button>`;
|
||||
}
|
||||
|
||||
// 页码
|
||||
for (let i = 1; i <= Math.min(totalPages, 5); i++) {
|
||||
const active = i === currentPage ? 'active' : '';
|
||||
paginationHtml += `<button class="page-btn ${active}" data-page="${i}">${i}</button>`;
|
||||
}
|
||||
|
||||
// 下一页
|
||||
if (currentPage < totalPages) {
|
||||
paginationHtml += `<button class="page-btn" data-page="${currentPage + 1}">下一页</button>`;
|
||||
}
|
||||
|
||||
paginationHtml += `</div>`;
|
||||
|
||||
$('#commentPagination').html(paginationHtml).show();
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
|
||||
const date = new Date(dateStr);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// 图片预览
|
||||
function previewImage(imgUrl) {
|
||||
// 创建预览模态框
|
||||
const modal = `
|
||||
<div class="image-preview-modal" onclick="this.remove()">
|
||||
<img src="${imgUrl}" alt="预览图片">
|
||||
</div>
|
||||
`;
|
||||
$('body').append(modal);
|
||||
}
|
||||
614
web/assets/js/product-list.js
Normal file
614
web/assets/js/product-list.js
Normal file
@@ -0,0 +1,614 @@
|
||||
// 商品列表页面逻辑 - 使用真实API
|
||||
|
||||
// 商品列表管理器
|
||||
const productList = {
|
||||
// 当前筛选和排序状态
|
||||
filters: {
|
||||
availability: [],
|
||||
priceMin: null,
|
||||
priceMax: null,
|
||||
category: []
|
||||
},
|
||||
sortBy: 'featured',
|
||||
currentPage: 1,
|
||||
itemsPerPage: 20,
|
||||
|
||||
// 所有商品数据
|
||||
totalCount: 0,
|
||||
|
||||
// 分类数据
|
||||
categories: [],
|
||||
|
||||
// 初始化
|
||||
init() {
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
|
||||
// 加载分类列表
|
||||
this.loadCategories();
|
||||
|
||||
// 加载商品列表
|
||||
this.loadProducts();
|
||||
|
||||
// 加载购物车数量
|
||||
this.loadCartCount();
|
||||
},
|
||||
|
||||
// 加载分类列表
|
||||
loadCategories() {
|
||||
console.log('加载分类列表');
|
||||
|
||||
API.get('/products/categories', { platform: 'web' })
|
||||
.then(data => {
|
||||
console.log('分类数据:', data);
|
||||
this.categories = data || [];
|
||||
this.renderCategories();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载分类失败:', error);
|
||||
// 失败时使用空数组,不影响页面其他功能
|
||||
this.categories = [];
|
||||
});
|
||||
},
|
||||
|
||||
// 渲染分类筛选器(京东风格 - 悬浮窗式)
|
||||
renderCategories() {
|
||||
if (this.categories.length === 0) {
|
||||
console.warn('分类数据为空');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('开始渲染分类,总数:', this.categories.length);
|
||||
|
||||
const lang = i18n.currentLang;
|
||||
const $categoryContainer = $('.filter-group:has(h4[data-i18n="filter_category"]) .filter-options');
|
||||
|
||||
let categoryHtml = '';
|
||||
|
||||
// 遍历一级分类
|
||||
this.categories.forEach((category, index) => {
|
||||
console.log(`渲染一级分类 [${index}]:`, category.id, category.name);
|
||||
|
||||
const categoryName = this.getCategoryName(category, lang);
|
||||
const isSelected = this.filters.category.includes(category.id.toString());
|
||||
|
||||
// 一级分类
|
||||
categoryHtml += `
|
||||
<div class="category-group">
|
||||
<div class="category-parent ${isSelected ? 'active' : ''}" data-category-id="${category.id}">
|
||||
<span class="category-name">${categoryName}</span>
|
||||
<span class="category-arrow">›</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 如果有二级分类,创建悬浮窗
|
||||
if (category.children && category.children.length > 0) {
|
||||
console.log(` 二级分类数量: ${category.children.length}`);
|
||||
categoryHtml += '<div class="category-popup">';
|
||||
categoryHtml += `<div class="category-popup-title">${categoryName}</div>`;
|
||||
categoryHtml += '<div class="category-popup-content">';
|
||||
|
||||
category.children.forEach((child, childIndex) => {
|
||||
console.log(` 渲染二级分类 [${childIndex}]:`, child.id, child.name);
|
||||
|
||||
const childName = this.getCategoryName(child, lang);
|
||||
const isChildSelected = this.filters.category.includes(child.id.toString());
|
||||
categoryHtml += `
|
||||
<span class="category-tag ${isChildSelected ? 'active' : ''}" data-category-id="${child.id}" data-parent="${category.id}">
|
||||
${childName}
|
||||
</span>
|
||||
`;
|
||||
});
|
||||
|
||||
categoryHtml += '</div></div>';
|
||||
}
|
||||
|
||||
categoryHtml += '</div>';
|
||||
});
|
||||
|
||||
console.log('分类 HTML 生成完成');
|
||||
$categoryContainer.html(categoryHtml);
|
||||
|
||||
// 绑定分类点击事件
|
||||
this.bindCategoryClick();
|
||||
|
||||
// 验证渲染结果
|
||||
console.log('渲染后的分类元素数量:', $('.category-parent').length);
|
||||
$('.category-parent').each(function(idx) {
|
||||
const id = $(this).data('category-id');
|
||||
const name = $(this).find('.category-name').text();
|
||||
console.log(` 分类 [${idx}]: ID=${id}, Name=${name}`);
|
||||
});
|
||||
},
|
||||
|
||||
// 绑定分类点击事件
|
||||
bindCategoryClick() {
|
||||
let hideTimeout = null;
|
||||
|
||||
// 鼠标进入分类组时,隐藏所有其他悬浮窗,只显示当前的
|
||||
$('.category-group').on('mouseenter', function() {
|
||||
clearTimeout(hideTimeout);
|
||||
|
||||
// 隐藏所有悬浮窗
|
||||
$('.category-popup').removeClass('show');
|
||||
|
||||
// 计算当前分类的位置
|
||||
const $parent = $(this).find('.category-parent');
|
||||
const parentRect = $parent[0].getBoundingClientRect();
|
||||
|
||||
// 只显示当前的悬浫窗
|
||||
const $popup = $(this).find('.category-popup');
|
||||
|
||||
// 设置悬浮窗位置(在一级分类右侧)
|
||||
$popup.css({
|
||||
left: parentRect.right + 12 + 'px', // 分类右侧 + 12px 间隙
|
||||
top: parentRect.top + 'px' // 与分类顶部对齐
|
||||
});
|
||||
|
||||
$popup.addClass('show');
|
||||
});
|
||||
|
||||
// 鼠标离开分类组时,延迟隐藏悬浮窗
|
||||
$('.category-group').on('mouseleave', function(e) {
|
||||
const $popup = $(this).find('.category-popup');
|
||||
|
||||
hideTimeout = setTimeout(() => {
|
||||
// 检查鼠标是否在悬浮窗内
|
||||
if (!$popup.is(':hover')) {
|
||||
$popup.removeClass('show');
|
||||
}
|
||||
}, 150); // 150ms 延迟
|
||||
});
|
||||
|
||||
// 鼠标进入悬浮窗时,取消隐藏
|
||||
$(document).on('mouseenter', '.category-popup', function() {
|
||||
clearTimeout(hideTimeout);
|
||||
$(this).addClass('show');
|
||||
});
|
||||
|
||||
// 鼠标离开悬浮窗时,隐藏
|
||||
$(document).on('mouseleave', '.category-popup', function() {
|
||||
$(this).removeClass('show');
|
||||
});
|
||||
|
||||
// 点击一级分类进行筛选(选择该分类下所有商品)
|
||||
$(document).on('click', '.category-parent', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const categoryId = $(this).data('category-id');
|
||||
console.log('点击一级分类,ID:', categoryId);
|
||||
|
||||
if (!categoryId) {
|
||||
console.error('分类 ID 不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryIdStr = categoryId.toString();
|
||||
const isActive = $(this).hasClass('active');
|
||||
|
||||
console.log('当前分类状态:', isActive ? '已选中' : '未选中');
|
||||
|
||||
// 移除所有选中状态
|
||||
$('.category-tag').removeClass('active');
|
||||
$('.category-parent').removeClass('active');
|
||||
|
||||
if (isActive) {
|
||||
// 如果已选中,则取消选中(查看全部)
|
||||
productList.filters.category = [];
|
||||
console.log('取消选中,查看全部商品');
|
||||
} else {
|
||||
// 选中当前一级分类
|
||||
$(this).addClass('active');
|
||||
productList.filters.category = [categoryIdStr];
|
||||
console.log('选中一级分类:', categoryIdStr);
|
||||
}
|
||||
|
||||
// 重新加载商品
|
||||
productList.currentPage = 1;
|
||||
productList.loadProducts();
|
||||
});
|
||||
|
||||
// 点击二级分类进行筛选
|
||||
$(document).on('click', '.category-tag', function(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const categoryId = $(this).data('category-id');
|
||||
console.log('点击二级分类,ID:', categoryId);
|
||||
|
||||
if (!categoryId) {
|
||||
console.error('分类 ID 不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
const categoryIdStr = categoryId.toString();
|
||||
const isActive = $(this).hasClass('active');
|
||||
|
||||
console.log('当前分类状态:', isActive ? '已选中' : '未选中');
|
||||
|
||||
// 移除所有选中状态
|
||||
$('.category-tag').removeClass('active');
|
||||
$('.category-parent').removeClass('active');
|
||||
|
||||
if (isActive) {
|
||||
// 如果已选中,则取消选中(查看全部)
|
||||
productList.filters.category = [];
|
||||
console.log('取消选中,查看全部商品');
|
||||
} else {
|
||||
// 选中当前分类
|
||||
$(this).addClass('active');
|
||||
productList.filters.category = [categoryIdStr];
|
||||
console.log('选中二级分类:', categoryIdStr);
|
||||
}
|
||||
|
||||
// 隐藏悬浮窗
|
||||
$('.category-popup').removeClass('show');
|
||||
|
||||
// 重新加载商品
|
||||
productList.currentPage = 1;
|
||||
productList.loadProducts();
|
||||
});
|
||||
},
|
||||
|
||||
// 获取分类名称(支持多语言)
|
||||
getCategoryName(category, lang) {
|
||||
// 如果有多语言数据
|
||||
if (category.name_i18n && category.name_i18n[lang]) {
|
||||
return category.name_i18n[lang];
|
||||
}
|
||||
// 降级使用默认名称
|
||||
return category.name || '未知分类';
|
||||
},
|
||||
|
||||
// 加载商品列表
|
||||
loadProducts() {
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
page_size: this.itemsPerPage
|
||||
};
|
||||
|
||||
// 添加价格筛选参数(元转分)
|
||||
if (this.filters.priceMin !== null && this.filters.priceMin > 0) {
|
||||
params.min_price = Math.round(this.filters.priceMin * 100);
|
||||
}
|
||||
if (this.filters.priceMax !== null && this.filters.priceMax > 0) {
|
||||
params.max_price = Math.round(this.filters.priceMax * 100);
|
||||
}
|
||||
|
||||
// 添加分类筛选参数
|
||||
if (this.filters.category && this.filters.category.length > 0) {
|
||||
// 如果后端支持多个分类,使用数组
|
||||
params.category_ids = this.filters.category.join(',');
|
||||
}
|
||||
|
||||
// 添加库存筛选参数
|
||||
if (this.filters.availability && this.filters.availability.length > 0) {
|
||||
if (this.filters.availability.includes('in_stock') && !this.filters.availability.includes('out_of_stock')) {
|
||||
params.in_stock = true;
|
||||
} else if (!this.filters.availability.includes('in_stock') && this.filters.availability.includes('out_of_stock')) {
|
||||
params.in_stock = false;
|
||||
}
|
||||
// 如果同时选中或都未选中,不添加该参数
|
||||
}
|
||||
|
||||
// 添加排序参数
|
||||
if (this.sortBy === 'price_asc') {
|
||||
params.sort = 'price';
|
||||
params.sortType = 0;
|
||||
} else if (this.sortBy === 'price_desc') {
|
||||
params.sort = 'price';
|
||||
params.sortType = 1;
|
||||
} else if (this.sortBy === 'date_new') {
|
||||
params.sort = 'created_at';
|
||||
params.sortType = 1;
|
||||
}
|
||||
|
||||
console.log('加载商品列表,参数:', params);
|
||||
|
||||
// 显示加载状态
|
||||
$('#productGrid').html('<div class="loading">加载中...</div>');
|
||||
|
||||
// 调用API
|
||||
API.get('/products', params)
|
||||
.then(data => {
|
||||
console.log('商品列表数据:', data);
|
||||
const products = data.list || [];
|
||||
this.totalCount = data.total || 0;
|
||||
|
||||
this.renderProducts(products);
|
||||
this.updateProductCount();
|
||||
this.renderPagination();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载商品列表失败:', error);
|
||||
$('#productGrid').html('<div class="error">加载失败,请稍后重试</div>');
|
||||
Toast.error(error.message || '加载商品失败');
|
||||
});
|
||||
},
|
||||
|
||||
// 渲染商品列表
|
||||
renderProducts(products) {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
if (products.length === 0) {
|
||||
// 美化的空状态
|
||||
$('#productGrid').html(`
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="60" cy="60" r="50" fill="#F5F5F5"/>
|
||||
<path d="M40 50 L50 60 L40 70 M80 50 L70 60 L80 70 M55 45 L65 75" stroke="#CCCCCC" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="empty-title">暂无商品</h3>
|
||||
<p class="empty-desc">请尝试调整筛选条件或清除筛选</p>
|
||||
<button class="btn btn-primary empty-btn" onclick="productList.clearFilters()">清除筛选</button>
|
||||
</div>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
const productsHtml = products.map(product => {
|
||||
const productName = product.name || '未知商品';
|
||||
const productImage = product.main_image || 'https://picsum.photos/400/400?random=default';
|
||||
const price = PriceUtils.fenToYuan(product.price || 0);
|
||||
const originalPrice = product.orig_price ? PriceUtils.fenToYuan(product.orig_price) : null;
|
||||
const isInStock = product.stock > 0;
|
||||
|
||||
// 计算折扣
|
||||
let discount = 0;
|
||||
if (originalPrice && originalPrice > price) {
|
||||
discount = Math.round((1 - price / originalPrice) * 100);
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="product-card" data-product-id="${product.id}">
|
||||
<div class="product-image">
|
||||
<a href="product-detail.html?id=${product.id}">
|
||||
<img src="${productImage}" alt="${productName}" loading="lazy">
|
||||
</a>
|
||||
${!isInStock ? '<span class="badge badge-sold-out">已售罄</span>' : ''}
|
||||
${discount > 0 ? `<span class="badge badge-sale">-${discount}%</span>` : ''}
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<h3 class="product-title">
|
||||
<a href="product-detail.html?id=${product.id}">${productName}</a>
|
||||
</h3>
|
||||
<div class="product-price">
|
||||
<span class="price-current">¥${price.toFixed(2)}</span>
|
||||
${originalPrice ? `<span class="price-original">¥${originalPrice.toFixed(2)}</span>` : ''}
|
||||
</div>
|
||||
<button class="btn btn-primary add-to-cart-btn" data-product-id="${product.id}" ${!isInStock ? 'disabled' : ''}>
|
||||
${isInStock ? '<span>加入购物车</span>' : '<span>已售罄</span>'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
$('#productGrid').html(productsHtml);
|
||||
},
|
||||
|
||||
// 更新商品计数
|
||||
updateProductCount() {
|
||||
$('#productCount').text(this.totalCount);
|
||||
},
|
||||
|
||||
// 渲染分页
|
||||
renderPagination() {
|
||||
const totalPages = Math.ceil(this.totalCount / this.itemsPerPage);
|
||||
|
||||
if (totalPages <= 1) {
|
||||
$('.pagination').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$('.pagination').show();
|
||||
|
||||
// 更新上一页按钮状态
|
||||
$('.page-btn.prev').prop('disabled', this.currentPage === 1);
|
||||
|
||||
// 更新下一页按钮状态
|
||||
$('.page-btn.next').prop('disabled', this.currentPage === totalPages);
|
||||
|
||||
// 渲染页码
|
||||
let pagesHtml = '';
|
||||
const maxPages = 7;
|
||||
let startPage = Math.max(1, this.currentPage - Math.floor(maxPages / 2));
|
||||
let endPage = Math.min(totalPages, startPage + maxPages - 1);
|
||||
|
||||
if (endPage - startPage < maxPages - 1) {
|
||||
startPage = Math.max(1, endPage - maxPages + 1);
|
||||
}
|
||||
|
||||
if (startPage > 1) {
|
||||
pagesHtml += `<button class="page-num" data-page="1">1</button>`;
|
||||
if (startPage > 2) {
|
||||
pagesHtml += `<span class="page-dots">...</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pagesHtml += `<button class="page-num ${i === this.currentPage ? 'active' : ''}" data-page="${i}">${i}</button>`;
|
||||
}
|
||||
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
pagesHtml += `<span class="page-dots">...</span>`;
|
||||
}
|
||||
pagesHtml += `<button class="page-num" data-page="${totalPages}">${totalPages}</button>`;
|
||||
}
|
||||
|
||||
$('.page-numbers').html(pagesHtml);
|
||||
},
|
||||
|
||||
// 绑定事件
|
||||
bindEvents() {
|
||||
// 筛选器事件
|
||||
$('.filter-options input[type="checkbox"]').on('change', () => {
|
||||
this.updateFilters();
|
||||
});
|
||||
|
||||
$('#priceMin, #priceMax').on('input', utils.debounce(() => {
|
||||
this.updateFilters();
|
||||
}, 500));
|
||||
|
||||
$('.apply-filter').on('click', () => {
|
||||
this.updateFilters();
|
||||
});
|
||||
|
||||
$('.clear-filter').on('click', () => {
|
||||
this.clearFilters();
|
||||
});
|
||||
|
||||
// 排序事件
|
||||
$('#sortSelect').on('change', (e) => {
|
||||
this.sortBy = $(e.target).val();
|
||||
this.currentPage = 1;
|
||||
this.loadProducts();
|
||||
});
|
||||
|
||||
// 分页事件
|
||||
$('.page-btn.prev').on('click', () => {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.loadProducts();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
$('.page-btn.next').on('click', () => {
|
||||
const totalPages = Math.ceil(this.totalCount / this.itemsPerPage);
|
||||
if (this.currentPage < totalPages) {
|
||||
this.currentPage++;
|
||||
this.loadProducts();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.page-num', (e) => {
|
||||
const page = parseInt($(e.target).data('page'));
|
||||
if (page && page !== this.currentPage) {
|
||||
this.currentPage = page;
|
||||
this.loadProducts();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
|
||||
// 加入购物车事件
|
||||
$(document).on('click', '.add-to-cart-btn', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const productId = $(e.currentTarget).data('product-id');
|
||||
this.addToCart(productId);
|
||||
});
|
||||
|
||||
// 监听语言切换
|
||||
$(document).on('languageChanged', () => {
|
||||
// 重新渲染分类(支持多语言)
|
||||
this.renderCategories();
|
||||
// 重新加载商品
|
||||
this.loadProducts();
|
||||
});
|
||||
},
|
||||
|
||||
// 更新筛选条件
|
||||
updateFilters() {
|
||||
// 获取库存状态
|
||||
this.filters.availability = [];
|
||||
$('input[name="availability"]:checked').each((index, el) => {
|
||||
this.filters.availability.push($(el).val());
|
||||
});
|
||||
|
||||
// 获取价格区间
|
||||
this.filters.priceMin = parseFloat($('#priceMin').val()) || null;
|
||||
this.filters.priceMax = parseFloat($('#priceMax').val()) || null;
|
||||
|
||||
// 分类由点击事件处理,不需要在这里更新
|
||||
|
||||
// 重置到第一页
|
||||
this.currentPage = 1;
|
||||
|
||||
// 重新加载商品
|
||||
this.loadProducts();
|
||||
},
|
||||
|
||||
// 清除筛选
|
||||
clearFilters() {
|
||||
$('input[type="checkbox"]').prop('checked', false);
|
||||
$('#priceMin, #priceMax').val('');
|
||||
|
||||
// 清除分类选中状态
|
||||
$('.category-tag').removeClass('active');
|
||||
$('.category-parent').removeClass('active');
|
||||
|
||||
this.filters = {
|
||||
availability: [],
|
||||
priceMin: null,
|
||||
priceMax: null,
|
||||
category: []
|
||||
};
|
||||
this.currentPage = 1;
|
||||
this.loadProducts();
|
||||
},
|
||||
|
||||
// 加入购物车
|
||||
addToCart(productId) {
|
||||
console.log('加入购物车:', productId);
|
||||
|
||||
API.post('/cart', {
|
||||
product_id: parseInt(productId),
|
||||
quantity: 1
|
||||
})
|
||||
.then(() => {
|
||||
Toast.success('已添加到购物车');
|
||||
this.loadCartCount();
|
||||
|
||||
// 打开购物车抽屉(如果有)
|
||||
if (typeof window.openCartDrawer === 'function') {
|
||||
window.openCartDrawer();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('添加到购物车失败:', error);
|
||||
Toast.error(error.message || '添加失败');
|
||||
});
|
||||
},
|
||||
|
||||
// 加载购物车数量
|
||||
loadCartCount() {
|
||||
// 检查是否登录,未登录时不请求
|
||||
const user = localStorage.getItem('currentUser');
|
||||
if (!user) {
|
||||
$('.cart-count').text(0);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userData = JSON.parse(user);
|
||||
if (!userData.token) {
|
||||
$('.cart-count').text(0);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
$('.cart-count').text(0);
|
||||
return;
|
||||
}
|
||||
|
||||
API.get('/cart')
|
||||
.then(data => {
|
||||
const totalQuantity = data.total_quantity || 0;
|
||||
$('.cart-count').text(totalQuantity);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载购物车数量失败:', error);
|
||||
$('.cart-count').text(0);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载完成后初始化
|
||||
$(document).ready(function() {
|
||||
productList.init();
|
||||
});
|
||||
425
web/assets/js/search.js
Normal file
425
web/assets/js/search.js
Normal file
@@ -0,0 +1,425 @@
|
||||
// 搜索页面逻辑
|
||||
const searchPage = {
|
||||
// 搜索参数
|
||||
keyword: '',
|
||||
filters: {
|
||||
priceMin: null,
|
||||
priceMax: null
|
||||
},
|
||||
sortBy: 'default',
|
||||
sortType: 'desc',
|
||||
currentPage: 1,
|
||||
itemsPerPage: 20,
|
||||
totalCount: 0,
|
||||
|
||||
// 初始化
|
||||
init() {
|
||||
console.log('=== 搜索页面初始化 ===');
|
||||
|
||||
// 从URL获取搜索关键词
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
this.keyword = urlParams.get('q') || '';
|
||||
|
||||
console.log('搜索关键词:', this.keyword);
|
||||
|
||||
if (!this.keyword) {
|
||||
// 如果没有搜索关键词,跳转回首页
|
||||
console.warn('没有搜索关键词,跳转到首页');
|
||||
window.location.href = 'index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示搜索关键词
|
||||
$('#searchKeyword').text(this.keyword);
|
||||
$('#searchInput').val(this.keyword);
|
||||
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
|
||||
// 执行搜索
|
||||
this.search();
|
||||
|
||||
// 加载购物车数量
|
||||
this.loadCartCount();
|
||||
},
|
||||
|
||||
// 绑定事件
|
||||
bindEvents() {
|
||||
// 排序选择
|
||||
$('#sortSelect').on('change', (e) => {
|
||||
const value = $(e.target).val();
|
||||
this.handleSort(value);
|
||||
});
|
||||
|
||||
// 应用筛选
|
||||
$('.apply-filter').on('click', () => {
|
||||
this.applyFilters();
|
||||
});
|
||||
|
||||
// 清除筛选
|
||||
$('.clear-filter').on('click', () => {
|
||||
this.clearFilters();
|
||||
});
|
||||
|
||||
// 价格输入框回车
|
||||
$('#priceMin, #priceMax').on('keypress', (e) => {
|
||||
if (e.which === 13) {
|
||||
this.applyFilters();
|
||||
}
|
||||
});
|
||||
|
||||
// 分页按钮
|
||||
$(document).on('click', '.page-btn.prev', () => {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.page-btn.next', () => {
|
||||
const totalPages = Math.ceil(this.totalCount / this.itemsPerPage);
|
||||
if (this.currentPage < totalPages) {
|
||||
this.currentPage++;
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.page-num', (e) => {
|
||||
const page = parseInt($(e.target).data('page'));
|
||||
if (page && page !== this.currentPage) {
|
||||
this.currentPage = page;
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索按钮
|
||||
$('.search-btn').on('click', () => {
|
||||
this.performNewSearch();
|
||||
});
|
||||
|
||||
// 搜索框回车
|
||||
$('#searchInput').on('keypress', (e) => {
|
||||
if (e.which === 13) {
|
||||
this.performNewSearch();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 执行新搜索
|
||||
performNewSearch() {
|
||||
const newKeyword = $('#searchInput').val().trim();
|
||||
if (newKeyword && newKeyword !== this.keyword) {
|
||||
window.location.href = `search.html?q=${encodeURIComponent(newKeyword)}`;
|
||||
}
|
||||
},
|
||||
|
||||
// 处理排序
|
||||
handleSort(sortValue) {
|
||||
console.log('排序选择:', sortValue);
|
||||
|
||||
switch (sortValue) {
|
||||
case 'price_asc':
|
||||
this.sortBy = 'price';
|
||||
this.sortType = 'asc';
|
||||
break;
|
||||
case 'price_desc':
|
||||
this.sortBy = 'price';
|
||||
this.sortType = 'desc';
|
||||
break;
|
||||
case 'sales_desc':
|
||||
this.sortBy = 'sales';
|
||||
this.sortType = 'desc';
|
||||
break;
|
||||
default:
|
||||
this.sortBy = 'default';
|
||||
this.sortType = 'desc';
|
||||
}
|
||||
|
||||
this.currentPage = 1;
|
||||
this.search();
|
||||
},
|
||||
|
||||
// 应用筛选
|
||||
applyFilters() {
|
||||
const minPrice = parseFloat($('#priceMin').val()) || null;
|
||||
const maxPrice = parseFloat($('#priceMax').val()) || null;
|
||||
|
||||
// 验证价格范围
|
||||
if (minPrice !== null && maxPrice !== null && minPrice > maxPrice) {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
message: i18n.t('error_price_range')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.filters.priceMin = minPrice;
|
||||
this.filters.priceMax = maxPrice;
|
||||
this.currentPage = 1;
|
||||
|
||||
console.log('应用筛选:', this.filters);
|
||||
this.search();
|
||||
},
|
||||
|
||||
// 清除筛选
|
||||
clearFilters() {
|
||||
this.filters.priceMin = null;
|
||||
this.filters.priceMax = null;
|
||||
$('#priceMin').val('');
|
||||
$('#priceMax').val('');
|
||||
this.currentPage = 1;
|
||||
|
||||
console.log('清除筛选');
|
||||
this.search();
|
||||
},
|
||||
|
||||
// 执行搜索
|
||||
search() {
|
||||
console.log('执行搜索:', {
|
||||
keyword: this.keyword,
|
||||
page: this.currentPage,
|
||||
filters: this.filters,
|
||||
sort: this.sortBy,
|
||||
sortType: this.sortType
|
||||
});
|
||||
|
||||
// 显示加载状态
|
||||
$('#productGrid').html('<div class="loading">加载中...</div>');
|
||||
|
||||
// 构建API参数
|
||||
const params = {
|
||||
keyword: this.keyword,
|
||||
page: this.currentPage,
|
||||
page_size: this.itemsPerPage
|
||||
};
|
||||
|
||||
// 添加价格筛选参数(元转分)
|
||||
if (this.filters.priceMin !== null) {
|
||||
params.min_price = Math.round(this.filters.priceMin * 100);
|
||||
}
|
||||
if (this.filters.priceMax !== null) {
|
||||
params.max_price = Math.round(this.filters.priceMax * 100);
|
||||
}
|
||||
|
||||
// 添加排序参数
|
||||
if (this.sortBy !== 'default') {
|
||||
params.sort_by = this.sortBy;
|
||||
params.sort_order = this.sortType;
|
||||
}
|
||||
|
||||
// 调用搜索API
|
||||
API.get('/products/search', params)
|
||||
.then(data => {
|
||||
console.log('搜索结果:', data);
|
||||
|
||||
if (data && data.list) {
|
||||
this.totalCount = data.total || 0;
|
||||
this.renderProducts(data.list);
|
||||
this.updateProductCount();
|
||||
this.renderPagination();
|
||||
} else {
|
||||
this.totalCount = 0;
|
||||
this.renderEmptyState();
|
||||
}
|
||||
|
||||
// 滚动到顶部
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('搜索失败:', error);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
message: error.message || i18n.t('error_load_products')
|
||||
});
|
||||
this.renderEmptyState();
|
||||
});
|
||||
},
|
||||
|
||||
// 渲染商品列表 (与product-list.js保持一致)
|
||||
renderProducts(products) {
|
||||
const lang = i18n.currentLang;
|
||||
|
||||
if (!products || products.length === 0) {
|
||||
this.renderEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
const productsHtml = products.map(product => {
|
||||
const productName = this.getProductName(product, lang) || '未知商品';
|
||||
const productImage = product.main_image || 'https://picsum.photos/400/400?random=default';
|
||||
|
||||
// 价格处理 - 后端API返回的是分,需要转换为元
|
||||
const price = (product.price || 0) / 100;
|
||||
const originalPrice = product.orig_price ? (product.orig_price / 100) : null;
|
||||
const isInStock = product.stock > 0;
|
||||
|
||||
// 计算折扣
|
||||
let discount = 0;
|
||||
if (originalPrice && originalPrice > price) {
|
||||
discount = Math.round((1 - price / originalPrice) * 100);
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="product-card" data-product-id="${product.id}">
|
||||
<div class="product-image">
|
||||
<a href="product-detail.html?id=${product.id}">
|
||||
<img src="${productImage}" alt="${productName}" loading="lazy">
|
||||
</a>
|
||||
${!isInStock ? '<span class="badge badge-sold-out">' + i18n.t('out_of_stock') + '</span>' : ''}
|
||||
${discount > 0 ? `<span class="badge badge-sale">-${discount}%</span>` : ''}
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<h3 class="product-title">
|
||||
<a href="product-detail.html?id=${product.id}">${productName}</a>
|
||||
</h3>
|
||||
<div class="product-price">
|
||||
<span class="price-current">¥${price.toFixed(2)}</span>
|
||||
${originalPrice ? `<span class="price-original">¥${originalPrice.toFixed(2)}</span>` : ''}
|
||||
</div>
|
||||
<button class="btn btn-primary add-to-cart-btn" data-product-id="${product.id}" ${!isInStock ? 'disabled' : ''}>
|
||||
${isInStock ? '<span>' + i18n.t('add_to_cart') + '</span>' : '<span>' + i18n.t('out_of_stock') + '</span>'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
$('#productGrid').html(productsHtml);
|
||||
|
||||
// 绑定加入购物车事件
|
||||
this.bindProductEvents();
|
||||
},
|
||||
|
||||
// 渲染空状态 (与product-list.js保持一致)
|
||||
renderEmptyState() {
|
||||
$('#productGrid').html(`
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="60" cy="60" r="50" fill="#F5F5F5"/>
|
||||
<path d="M40 50 L50 60 L40 70 M80 50 L70 60 L80 70 M55 45 L65 75" stroke="#CCCCCC" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="empty-title">${i18n.t('search_no_results')}</h3>
|
||||
<p class="empty-desc">${i18n.t('search_try_again')}</p>
|
||||
<a href="home.html" class="btn btn-primary empty-btn">${i18n.t('browse_all_products')}</a>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$('.pagination').hide();
|
||||
},
|
||||
|
||||
// 更新商品计数
|
||||
updateProductCount() {
|
||||
$('#productCount').text(this.totalCount);
|
||||
},
|
||||
|
||||
// 渲染分页 (与product-list.js保持一致)
|
||||
renderPagination() {
|
||||
const totalPages = Math.ceil(this.totalCount / this.itemsPerPage);
|
||||
|
||||
if (totalPages <= 1) {
|
||||
$('.pagination').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$('.pagination').show();
|
||||
|
||||
// 更新上一页按钮状态
|
||||
$('.page-btn.prev').prop('disabled', this.currentPage === 1);
|
||||
|
||||
// 更新下一页按钮状态
|
||||
$('.page-btn.next').prop('disabled', this.currentPage === totalPages);
|
||||
|
||||
// 渲染页码
|
||||
let pagesHtml = '';
|
||||
const maxPages = 7;
|
||||
let startPage = Math.max(1, this.currentPage - Math.floor(maxPages / 2));
|
||||
let endPage = Math.min(totalPages, startPage + maxPages - 1);
|
||||
|
||||
if (endPage - startPage < maxPages - 1) {
|
||||
startPage = Math.max(1, endPage - maxPages + 1);
|
||||
}
|
||||
|
||||
if (startPage > 1) {
|
||||
pagesHtml += `<button class="page-num" data-page="1">1</button>`;
|
||||
if (startPage > 2) {
|
||||
pagesHtml += `<span class="page-dots">...</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pagesHtml += `<button class="page-num ${i === this.currentPage ? 'active' : ''}" data-page="${i}">${i}</button>`;
|
||||
}
|
||||
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
pagesHtml += `<span class="page-dots">...</span>`;
|
||||
}
|
||||
pagesHtml += `<button class="page-num" data-page="${totalPages}">${totalPages}</button>`;
|
||||
}
|
||||
|
||||
$('.page-numbers').html(pagesHtml);
|
||||
},
|
||||
|
||||
// 绑定商品事件
|
||||
bindProductEvents() {
|
||||
$('.add-to-cart-btn').off('click').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const productId = $(e.currentTarget).data('product-id');
|
||||
this.addToCart(productId);
|
||||
});
|
||||
},
|
||||
|
||||
// 加入购物车
|
||||
addToCart(productId) {
|
||||
console.log('加入购物车:', productId);
|
||||
|
||||
// 检查是否登录
|
||||
if (!cart.isLoggedIn()) {
|
||||
Toast.warning(i18n.t('please_login_first'));
|
||||
setTimeout(() => {
|
||||
window.location.href = 'login.html?redirect=' + encodeURIComponent(window.location.href);
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用购物车API
|
||||
CartAPI.addToCart(productId, 0, 1)
|
||||
.then(() => {
|
||||
Toast.success(i18n.t('add_to_cart_success'));
|
||||
|
||||
// 更新购物车数量
|
||||
cart.updateCartCount();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加入购物车失败:', error);
|
||||
Toast.error(error.message || i18n.t('add_to_cart_failed'));
|
||||
});
|
||||
},
|
||||
|
||||
// 加载购物车数量
|
||||
loadCartCount() {
|
||||
if (cart && typeof cart.updateCartCount === 'function') {
|
||||
cart.updateCartCount();
|
||||
}
|
||||
},
|
||||
|
||||
// 获取商品名称(多语言)
|
||||
getProductName(product, lang) {
|
||||
if (lang === 'en-US' && product.name_en) {
|
||||
return product.name_en;
|
||||
}
|
||||
if (lang === 'ja-JP' && product.name_ja) {
|
||||
return product.name_ja;
|
||||
}
|
||||
return product.name || '';
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载完成后初始化
|
||||
$(document).ready(function() {
|
||||
searchPage.init();
|
||||
});
|
||||
496
web/assets/js/toast.js
Normal file
496
web/assets/js/toast.js
Normal file
@@ -0,0 +1,496 @@
|
||||
/**
|
||||
* Toast 消息提醒组件 - iOS风格
|
||||
* 使用方法:
|
||||
* Toast.show('消息内容')
|
||||
* Toast.success('成功消息')
|
||||
* Toast.error('错误消息')
|
||||
* Toast.warning('警告消息')
|
||||
* Toast.info('提示消息')
|
||||
*/
|
||||
|
||||
const Toast = (function() {
|
||||
// 消息类型配置
|
||||
const TYPES = {
|
||||
success: {
|
||||
icon: '✓',
|
||||
color: '#34C759',
|
||||
iconBg: 'rgba(52, 199, 89, 0.15)'
|
||||
},
|
||||
error: {
|
||||
icon: '✕',
|
||||
color: '#FF3B30',
|
||||
iconBg: 'rgba(255, 59, 48, 0.15)'
|
||||
},
|
||||
warning: {
|
||||
icon: '!',
|
||||
color: '#FF9500',
|
||||
iconBg: 'rgba(255, 149, 0, 0.15)'
|
||||
},
|
||||
info: {
|
||||
icon: 'i',
|
||||
color: '#007AFF',
|
||||
iconBg: 'rgba(0, 122, 255, 0.15)'
|
||||
}
|
||||
};
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_OPTIONS = {
|
||||
duration: 2500, // 显示时长(毫秒)
|
||||
position: 'top-center', // 位置:top-center, top-right, bottom-center, center
|
||||
showIcon: true, // 是否显示图标
|
||||
animation: true // 是否使用动画
|
||||
};
|
||||
|
||||
// 当前显示的toast队列
|
||||
let toastQueue = [];
|
||||
let container = null;
|
||||
|
||||
/**
|
||||
* 初始化容器
|
||||
*/
|
||||
function initContainer() {
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Toast元素
|
||||
*/
|
||||
function createToast(message, type = 'info', options = {}) {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
const config = TYPES[type] || TYPES.info;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type} toast-${opts.position}`;
|
||||
|
||||
if (opts.animation) {
|
||||
toast.classList.add('toast-enter');
|
||||
}
|
||||
|
||||
// 构建toast内容
|
||||
let html = '<div class="toast-content">';
|
||||
|
||||
if (opts.showIcon) {
|
||||
html += `
|
||||
<div class="toast-icon" style="background-color: ${config.iconBg}; color: ${config.color}">
|
||||
<span>${config.icon}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="toast-message">${message}</div>
|
||||
</div>`;
|
||||
|
||||
toast.innerHTML = html;
|
||||
|
||||
return { element: toast, duration: opts.duration };
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示Toast
|
||||
*/
|
||||
function show(message, type = 'info', options = {}) {
|
||||
const toastContainer = initContainer();
|
||||
const { element, duration } = createToast(message, type, options);
|
||||
|
||||
// 添加到DOM
|
||||
toastContainer.appendChild(element);
|
||||
toastQueue.push(element);
|
||||
|
||||
// 触发入场动画
|
||||
setTimeout(() => {
|
||||
element.classList.remove('toast-enter');
|
||||
element.classList.add('toast-visible');
|
||||
}, 10);
|
||||
|
||||
// 自动移除
|
||||
setTimeout(() => {
|
||||
hide(element);
|
||||
}, duration);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏Toast
|
||||
*/
|
||||
function hide(toastElement) {
|
||||
if (!toastElement || !toastElement.parentNode) return;
|
||||
|
||||
toastElement.classList.remove('toast-visible');
|
||||
toastElement.classList.add('toast-exit');
|
||||
|
||||
setTimeout(() => {
|
||||
if (toastElement.parentNode) {
|
||||
toastElement.parentNode.removeChild(toastElement);
|
||||
}
|
||||
|
||||
// 从队列中移除
|
||||
const index = toastQueue.indexOf(toastElement);
|
||||
if (index > -1) {
|
||||
toastQueue.splice(index, 1);
|
||||
}
|
||||
|
||||
// 如果队列为空,移除容器
|
||||
if (toastQueue.length === 0 && container) {
|
||||
container.remove();
|
||||
container = null;
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有Toast
|
||||
*/
|
||||
function clear() {
|
||||
toastQueue.forEach(toast => hide(toast));
|
||||
toastQueue = [];
|
||||
}
|
||||
|
||||
// 快捷方法
|
||||
function success(message, options) {
|
||||
return show(message, 'success', options);
|
||||
}
|
||||
|
||||
function error(message, options) {
|
||||
return show(message, 'error', options);
|
||||
}
|
||||
|
||||
function warning(message, options) {
|
||||
return show(message, 'warning', options);
|
||||
}
|
||||
|
||||
function info(message, options) {
|
||||
return show(message, 'info', options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提示对话框(iOS风格,只有一个确定按钮)
|
||||
*/
|
||||
function alert(options = {}) {
|
||||
// 支持直接传字符串或对象
|
||||
if (typeof options === 'string') {
|
||||
options = { message: options };
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const opts = {
|
||||
title: options.title || '',
|
||||
message: options.message || '',
|
||||
confirmText: options.confirmText || i18n?.t?.('confirm') || '确定',
|
||||
confirmColor: options.confirmColor || '#007AFF',
|
||||
...options
|
||||
};
|
||||
|
||||
// 创建遮罩层
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'toast-overlay';
|
||||
|
||||
// 创建对话框
|
||||
const dialog = document.createElement('div');
|
||||
dialog.className = 'toast-dialog';
|
||||
|
||||
let html = '<div class="toast-dialog-content">';
|
||||
|
||||
if (opts.title) {
|
||||
html += `<div class="toast-dialog-title">${opts.title}</div>`;
|
||||
}
|
||||
|
||||
if (opts.message) {
|
||||
html += `<div class="toast-dialog-message">${opts.message}</div>`;
|
||||
}
|
||||
|
||||
// 只有一个确定按钮
|
||||
html += `
|
||||
<div class="toast-dialog-buttons toast-dialog-single-button">
|
||||
<button class="toast-dialog-button toast-dialog-confirm" style="color: ${opts.confirmColor}">
|
||||
${opts.confirmText}
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
dialog.innerHTML = html;
|
||||
|
||||
// 添加到DOM
|
||||
overlay.appendChild(dialog);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// 触发动画
|
||||
setTimeout(() => {
|
||||
overlay.classList.add('toast-overlay-visible');
|
||||
dialog.classList.add('toast-dialog-visible');
|
||||
}, 10);
|
||||
|
||||
// 按钮事件
|
||||
const confirmBtn = dialog.querySelector('.toast-dialog-confirm');
|
||||
|
||||
function close() {
|
||||
overlay.classList.remove('toast-overlay-visible');
|
||||
dialog.classList.remove('toast-dialog-visible');
|
||||
setTimeout(() => {
|
||||
overlay.remove();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
close();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
// 点击遮罩关闭
|
||||
if (opts.closeOnClickOverlay !== false) {
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) {
|
||||
close();
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认对话框(iOS风格)
|
||||
*/
|
||||
function confirm(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const opts = {
|
||||
title: options.title || '',
|
||||
message: options.message || '',
|
||||
confirmText: options.confirmText || i18n.t('confirm') || '确定',
|
||||
cancelText: options.cancelText || i18n.t('cancel') || '取消',
|
||||
confirmColor: options.confirmColor || '#007AFF',
|
||||
cancelColor: options.cancelColor || '#8E8E93',
|
||||
...options
|
||||
};
|
||||
|
||||
// 创建遮罩层
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'toast-overlay';
|
||||
|
||||
// 创建对话框
|
||||
const dialog = document.createElement('div');
|
||||
dialog.className = 'toast-dialog';
|
||||
|
||||
let html = '<div class="toast-dialog-content">';
|
||||
|
||||
if (opts.title) {
|
||||
html += `<div class="toast-dialog-title">${opts.title}</div>`;
|
||||
}
|
||||
|
||||
if (opts.message) {
|
||||
html += `<div class="toast-dialog-message">${opts.message}</div>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="toast-dialog-buttons">
|
||||
<button class="toast-dialog-button toast-dialog-cancel" style="color: ${opts.cancelColor}">
|
||||
${opts.cancelText}
|
||||
</button>
|
||||
<button class="toast-dialog-button toast-dialog-confirm" style="color: ${opts.confirmColor}">
|
||||
${opts.confirmText}
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
dialog.innerHTML = html;
|
||||
|
||||
// 添加到DOM
|
||||
overlay.appendChild(dialog);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// 触发动画
|
||||
setTimeout(() => {
|
||||
overlay.classList.add('toast-overlay-visible');
|
||||
dialog.classList.add('toast-dialog-visible');
|
||||
}, 10);
|
||||
|
||||
// 按钮事件
|
||||
const confirmBtn = dialog.querySelector('.toast-dialog-confirm');
|
||||
const cancelBtn = dialog.querySelector('.toast-dialog-cancel');
|
||||
|
||||
function close() {
|
||||
overlay.classList.remove('toast-overlay-visible');
|
||||
dialog.classList.remove('toast-dialog-visible');
|
||||
setTimeout(() => {
|
||||
overlay.remove();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
close();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
close();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
// 点击遮罩关闭
|
||||
if (opts.closeOnClickOverlay !== false) {
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) {
|
||||
close();
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入对话框(iOS风格)
|
||||
*/
|
||||
function prompt(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const opts = {
|
||||
title: options.title || '',
|
||||
message: options.message || '',
|
||||
placeholder: options.placeholder || '',
|
||||
defaultValue: options.defaultValue || '',
|
||||
inputType: options.inputType || 'text',
|
||||
confirmText: options.confirmText || i18n.t('confirm') || '确定',
|
||||
cancelText: options.cancelText || i18n.t('cancel') || '取消',
|
||||
confirmColor: options.confirmColor || '#007AFF',
|
||||
cancelColor: options.cancelColor || '#8E8E93',
|
||||
maxLength: options.maxLength || null,
|
||||
...options
|
||||
};
|
||||
|
||||
// 创建遮罩层
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'toast-overlay';
|
||||
|
||||
// 创建对话框
|
||||
const dialog = document.createElement('div');
|
||||
dialog.className = 'toast-dialog toast-prompt-dialog';
|
||||
|
||||
let html = '<div class="toast-dialog-content">';
|
||||
|
||||
if (opts.title) {
|
||||
html += `<div class="toast-dialog-title">${opts.title}</div>`;
|
||||
}
|
||||
|
||||
if (opts.message) {
|
||||
html += `<div class="toast-dialog-message">${opts.message}</div>`;
|
||||
}
|
||||
|
||||
// 输入框
|
||||
const maxLengthAttr = opts.maxLength ? `maxlength="${opts.maxLength}"` : '';
|
||||
html += `
|
||||
<div class="toast-input-wrapper">
|
||||
<input
|
||||
type="${opts.inputType}"
|
||||
class="toast-input"
|
||||
placeholder="${opts.placeholder}"
|
||||
value="${opts.defaultValue}"
|
||||
${maxLengthAttr}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
html += `
|
||||
<div class="toast-dialog-buttons">
|
||||
<button class="toast-dialog-button toast-dialog-cancel" style="color: ${opts.cancelColor}">
|
||||
${opts.cancelText}
|
||||
</button>
|
||||
<button class="toast-dialog-button toast-dialog-confirm" style="color: ${opts.confirmColor}">
|
||||
${opts.confirmText}
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
dialog.innerHTML = html;
|
||||
|
||||
// 添加到DOM
|
||||
overlay.appendChild(dialog);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// 获取输入框
|
||||
const input = dialog.querySelector('.toast-input');
|
||||
const confirmBtn = dialog.querySelector('.toast-dialog-confirm');
|
||||
const cancelBtn = dialog.querySelector('.toast-dialog-cancel');
|
||||
|
||||
// 触发动画
|
||||
setTimeout(() => {
|
||||
overlay.classList.add('toast-overlay-visible');
|
||||
dialog.classList.add('toast-dialog-visible');
|
||||
// 自动聚焦并选中文本
|
||||
input.focus();
|
||||
if (opts.defaultValue) {
|
||||
input.select();
|
||||
}
|
||||
}, 10);
|
||||
|
||||
function close() {
|
||||
overlay.classList.remove('toast-overlay-visible');
|
||||
dialog.classList.remove('toast-dialog-visible');
|
||||
setTimeout(() => {
|
||||
overlay.remove();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
const value = input.value;
|
||||
close();
|
||||
resolve(value);
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
close();
|
||||
resolve(null);
|
||||
}
|
||||
|
||||
// 按钮事件
|
||||
confirmBtn.addEventListener('click', handleConfirm);
|
||||
cancelBtn.addEventListener('click', handleCancel);
|
||||
|
||||
// 回车确认
|
||||
input.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleConfirm();
|
||||
}
|
||||
});
|
||||
|
||||
// ESC取消
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
}
|
||||
});
|
||||
|
||||
// 点击遮罩关闭
|
||||
if (opts.closeOnClickOverlay !== false) {
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) {
|
||||
handleCancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 导出API
|
||||
return {
|
||||
show,
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
alert,
|
||||
confirm,
|
||||
prompt,
|
||||
clear,
|
||||
hide
|
||||
};
|
||||
})();
|
||||
|
||||
// 全局暴露
|
||||
window.Toast = Toast;
|
||||
1368
web/assets/js/user-center.js
Normal file
1368
web/assets/js/user-center.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user