This commit is contained in:
sjk
2025-11-28 15:18:10 +08:00
parent ad4a600af9
commit 5683f35942
188 changed files with 53680 additions and 1062 deletions

588
web/assets/js/api.js Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

584
web/assets/js/home.js Normal file
View 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

File diff suppressed because it is too large Load Diff

773
web/assets/js/live-float.js Normal file
View 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
View 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
View 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
View 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);
}

View 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);
}

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff