Compare commits

...

3 Commits

Author SHA1 Message Date
zhinianboke
43351ead93 Update wechat-group.png 2025-08-06 18:44:11 +08:00
zhinianboke
20ff87f47b Update README.md 2025-08-06 15:28:17 +08:00
zhinianboke
4f16a51087 新增商品搜索功能 2025-08-06 15:25:22 +08:00
8 changed files with 359 additions and 384 deletions

View File

@ -1,121 +0,0 @@
# 自动回复暂停功能说明
## 功能概述
当系统检测到某个 `chat_id` 有手动发出的消息时,会自动暂停该 `chat_id` 的自动回复功能10分钟。如果在暂停期间再次检测到手动发出的消息会重新开始计时10分钟。
## 功能特性
### 1. 智能检测
- 系统会自动检测每个聊天会话中的手动发出消息
- 检测到手动发出时会在日志中显示:`[时间] 【手动发出】 商品(商品ID): 消息内容`
### 2. 自动暂停
- 检测到手动发出后,立即暂停该 `chat_id` 的自动回复10分钟
- 暂停期间会在日志中显示:`【账号ID】检测到手动发出消息chat_id XXX 自动回复暂停10分钟恢复时间: YYYY-MM-DD HH:MM:SS`
### 3. 重新计时
- 如果在暂停期间再次检测到手动发出会重新开始计时10分钟
- 每次手动发出都会刷新暂停时间
### 4. 暂停提示
- 当收到消息但处于暂停状态时,会在日志中显示:
`【账号ID】【系统】chat_id XXX 自动回复已暂停,剩余时间: X分Y秒`
### 5. 自动恢复
- 暂停时间到期后,自动恢复该 `chat_id` 的自动回复功能
- 无需手动干预
## 技术实现
### 核心组件
#### AutoReplyPauseManager 类
- `pause_chat(chat_id, cookie_id)`: 暂停指定chat_id的自动回复
- `is_chat_paused(chat_id)`: 检查指定chat_id是否处于暂停状态
- `get_remaining_pause_time(chat_id)`: 获取剩余暂停时间
- `cleanup_expired_pauses()`: 清理过期的暂停记录
#### 集成点
1. **检测手动发出** (第2730行)
```python
if send_user_id == self.myid:
logger.info(f"[{msg_time}] 【手动发出】 商品({item_id}): {send_message}")
# 暂停该chat_id的自动回复10分钟
pause_manager.pause_chat(chat_id, self.cookie_id)
return
```
2. **检查暂停状态** (第2750行)
```python
# 检查该chat_id是否处于暂停状态
if pause_manager.is_chat_paused(chat_id):
remaining_time = pause_manager.get_remaining_pause_time(chat_id)
remaining_minutes = remaining_time // 60
remaining_seconds = remaining_time % 60
logger.info(f"[{msg_time}] 【{self.cookie_id}】【系统】chat_id {chat_id} 自动回复已暂停,剩余时间: {remaining_minutes}分{remaining_seconds}秒")
return
```
3. **定期清理** (第2372行)
```python
async def pause_cleanup_loop(self):
"""定期清理过期的暂停记录"""
while True:
# 每5分钟清理一次过期记录
pause_manager.cleanup_expired_pauses()
await asyncio.sleep(300)
```
## 配置参数
### 暂停时长
- 默认10分钟 (600秒)
- 位置:`AutoReplyPauseManager.__init__()` 中的 `self.pause_duration`
- 可根据需要修改
### 清理频率
- 默认每5分钟清理一次过期记录
- 位置:`pause_cleanup_loop()` 中的 `await asyncio.sleep(300)`
## 日志示例
### 检测到手动发出
```
2025-08-05 14:48:32.209 | INFO | XianyuAutoAsync:handle_message:2673 - [2025-08-05 14:48:31] 【手动发出】 商品(12345): 你好,这个商品还在吗?
2025-08-05 14:48:32.210 | INFO | XianyuAutoAsync:pause_chat:40 - 【dfg】检测到手动发出消息chat_id chat_123 自动回复暂停10分钟恢复时间: 2025-08-05 14:58:32
```
### 暂停期间收到消息
```
2025-08-05 14:50:15.123 | INFO | XianyuAutoAsync:handle_message:2678 - [2025-08-05 14:50:15] 【收到】用户: 张三 (ID: 67890), 商品(12345): 多少钱?
2025-08-05 14:50:15.124 | INFO | XianyuAutoAsync:handle_message:2754 - [2025-08-05 14:50:15] 【dfg】【系统】chat_id chat_123 自动回复已暂停,剩余时间: 8分17秒
```
### 重新计时
```
2025-08-05 14:55:20.456 | INFO | XianyuAutoAsync:handle_message:2673 - [2025-08-05 14:55:20] 【手动发出】 商品(12345): 价格可以商量
2025-08-05 14:55:20.457 | INFO | XianyuAutoAsync:pause_chat:40 - 【dfg】检测到手动发出消息chat_id chat_123 自动回复暂停10分钟恢复时间: 2025-08-05 15:05:20
```
## 注意事项
1. **全局管理器**: 使用全局的 `pause_manager` 实例,所有账号共享暂停状态
2. **内存存储**: 暂停记录存储在内存中,程序重启后会丢失
3. **自动清理**: 系统会定期清理过期的暂停记录,避免内存泄漏
4. **线程安全**: 暂停管理器是线程安全的,可以在多个协程中使用
## 测试
运行测试脚本验证功能:
```bash
python test_pause_manager.py
```
测试包括:
- 基本暂停/恢复功能
- 重新计时机制
- 多chat_id管理
- 过期清理功能
- 时间计算准确性

View File

@ -1,185 +0,0 @@
# 账号自动回复暂停时间配置功能
## 功能概述
为每个账号单独配置自动回复暂停时间,当检测到手动发出消息后,该账号的自动回复会暂停指定的时间长度。
## 功能特性
### 1. 个性化配置
- 每个账号可以单独设置暂停时间1-60分钟
- 默认暂停时间为10分钟
- 支持实时修改,立即生效
### 2. 直观界面
- 在账号管理表格中新增"暂停时间"列
- 点击暂停时间可直接编辑
- 带有说明工具提示,解释功能作用
### 3. 智能验证
- 暂停时间范围限制1-60分钟
- 输入验证和错误提示
- 支持键盘操作Enter保存Escape取消
## 界面展示
### 表格列头
```
账号ID | Cookie值 | 关键词 | 状态 | 默认回复 | AI回复 | 自动确认发货 | 备注 | 暂停时间 | 操作
```
### 暂停时间列
- 显示格式:`🕐 10分钟`
- 工具提示:`检测到手动发出消息后,自动回复暂停的时间长度(分钟)。如果在暂停期间再次手动发出消息,会重新开始计时。`
- 点击可编辑,支持数字输入框
## 技术实现
### 数据库结构
```sql
-- cookies表新增字段
ALTER TABLE cookies ADD COLUMN pause_duration INTEGER DEFAULT 10;
```
### 后端API
#### 1. 更新暂停时间
```http
PUT /cookies/{cid}/pause-duration
Content-Type: application/json
Authorization: Bearer {token}
{
"pause_duration": 15
}
```
**响应**
```json
{
"message": "暂停时间更新成功",
"pause_duration": 15
}
```
#### 2. 获取暂停时间
```http
GET /cookies/{cid}/pause-duration
Authorization: Bearer {token}
```
**响应**
```json
{
"pause_duration": 15,
"message": "获取暂停时间成功"
}
```
### 前端功能
#### 1. 表格显示
- 在账号列表中显示每个账号的暂停时间
- 默认显示为"🕐 10分钟"格式
#### 2. 内联编辑
```javascript
function editPauseDuration(cookieId, currentDuration) {
// 创建数字输入框
// 支持1-60分钟范围
// Enter保存Escape取消
// 实时验证和错误提示
}
```
#### 3. 工具提示
- 使用Bootstrap Tooltip组件
- 自动初始化和重新初始化
### 暂停管理器集成
#### 动态获取暂停时间
```python
def pause_chat(self, chat_id: str, cookie_id: str):
"""暂停指定chat_id的自动回复使用账号特定的暂停时间"""
# 获取账号特定的暂停时间
try:
from db_manager import db_manager
pause_minutes = db_manager.get_cookie_pause_duration(cookie_id)
except Exception as e:
logger.error(f"获取账号 {cookie_id} 暂停时间失败: {e}使用默认10分钟")
pause_minutes = 10
pause_duration_seconds = pause_minutes * 60
pause_until = time.time() + pause_duration_seconds
self.paused_chats[chat_id] = pause_until
# 记录日志
end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(pause_until))
logger.info(f"【{cookie_id}】检测到手动发出消息chat_id {chat_id} 自动回复暂停{pause_minutes}分钟,恢复时间: {end_time}")
```
## 使用流程
### 1. 查看当前设置
1. 登录系统,进入账号管理页面
2. 在表格中查看"暂停时间"列
3. 鼠标悬停在列头的问号图标上查看功能说明
### 2. 修改暂停时间
1. 点击要修改的账号的暂停时间(如"🕐 10分钟"
2. 输入框出现输入新的暂停时间1-60分钟
3. 按Enter键保存或按Escape键取消
4. 系统显示成功提示
### 3. 验证生效
1. 修改后的设置立即生效
2. 下次该账号检测到手动发出消息时,会使用新的暂停时间
3. 在日志中可以看到使用的暂停时间
## 日志示例
### 使用自定义暂停时间
```
2025-08-05 15:30:15.123 | INFO | XianyuAutoAsync:pause_chat:49 - 【abc123】检测到手动发出消息chat_id chat_456 自动回复暂停15分钟恢复时间: 2025-08-05 15:45:15
```
### 获取暂停时间失败时使用默认值
```
2025-08-05 15:30:15.124 | ERROR | XianyuAutoAsync:pause_chat:42 - 获取账号 abc123 暂停时间失败: Database error使用默认10分钟
2025-08-05 15:30:15.125 | INFO | XianyuAutoAsync:pause_chat:49 - 【abc123】检测到手动发出消息chat_id chat_456 自动回复暂停10分钟恢复时间: 2025-08-05 15:40:15
```
## 配置建议
### 不同场景的推荐设置
1. **高频互动商品**5-10分钟
- 适用于需要快速响应的热门商品
- 减少客户等待时间
2. **普通商品**10-15分钟默认
- 平衡自动化和人工干预
- 适合大多数场景
3. **低频互动商品**20-30分钟
- 适用于不常有咨询的商品
- 给予更多人工处理时间
4. **特殊商品**30-60分钟
- 需要详细沟通的复杂商品
- 避免自动回复干扰深度交流
## 注意事项
1. **范围限制**暂停时间必须在1-60分钟之间
2. **立即生效**:修改后的设置立即生效,无需重启
3. **默认值**新账号默认使用10分钟暂停时间
4. **错误处理**获取暂停时间失败时自动使用默认10分钟
5. **权限控制**:只能修改自己账号的暂停时间设置
## 兼容性
- **向后兼容**现有账号自动获得默认10分钟设置
- **数据迁移**系统启动时自动添加pause_duration字段
- **API兼容**现有API继续正常工作新增字段可选

View File

@ -1,4 +1,4 @@
# 🐟 闲鱼自动回复系统
# 🐟 闲鱼自动回复系统 另外提供cursor pro 14天试用账号物美价廉欢迎联系
[![GitHub](https://img.shields.io/badge/GitHub-zhinianboke%2Fxianyu--auto--reply-blue?logo=github)](https://github.com/zhinianboke/xianyu-auto-reply)
[![Docker](https://img.shields.io/badge/Docker-一键部署-blue?logo=docker)](https://github.com/zhinianboke/xianyu-auto-reply#-快速开始)

View File

@ -3658,9 +3658,10 @@ def get_table_data(table_name: str, admin_user: Dict[str, Any] = Depends(require
# 验证表名安全性
allowed_tables = [
'users', 'cookies', 'keywords', 'default_replies', 'ai_reply_settings',
'users', 'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records',
'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info',
'message_notifications', 'cards', 'delivery_rules', 'notification_channels',
'user_settings', 'email_verifications', 'captcha_codes'
'user_settings', 'system_settings', 'email_verifications', 'captcha_codes'
]
if table_name not in allowed_tables:
@ -3694,9 +3695,10 @@ def delete_table_record(table_name: str, record_id: str, admin_user: Dict[str, A
# 验证表名安全性
allowed_tables = [
'users', 'cookies', 'keywords', 'default_replies', 'ai_reply_settings',
'users', 'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records',
'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info',
'message_notifications', 'cards', 'delivery_rules', 'notification_channels',
'user_settings', 'email_verifications', 'captcha_codes'
'user_settings', 'system_settings', 'email_verifications', 'captcha_codes'
]
if table_name not in allowed_tables:
@ -3733,9 +3735,10 @@ def clear_table_data(table_name: str, admin_user: Dict[str, Any] = Depends(requi
# 验证表名安全性
allowed_tables = [
'cookies', 'keywords', 'default_replies', 'ai_reply_settings',
'cookies', 'cookie_status', 'keywords', 'default_replies', 'default_reply_records',
'ai_reply_settings', 'ai_conversations', 'ai_item_cache', 'item_info',
'message_notifications', 'cards', 'delivery_rules', 'notification_channels',
'user_settings', 'email_verifications', 'captcha_codes'
'user_settings', 'system_settings', 'email_verifications', 'captcha_codes'
]
# 不允许清空用户表

View File

@ -95,14 +95,20 @@
<option value="">请选择数据表...</option>
<option value="users">users - 用户表</option>
<option value="cookies">cookies - Cookie账号表</option>
<option value="cookie_status">cookie_status - Cookie状态表</option>
<option value="keywords">keywords - 关键字表</option>
<option value="default_replies">default_replies - 默认回复表</option>
<option value="default_reply_records">default_reply_records - 默认回复记录表</option>
<option value="ai_reply_settings">ai_reply_settings - AI回复设置表</option>
<option value="ai_conversations">ai_conversations - AI对话历史表</option>
<option value="ai_item_cache">ai_item_cache - AI商品信息缓存表</option>
<option value="item_info">item_info - 商品信息表</option>
<option value="message_notifications">message_notifications - 消息通知表</option>
<option value="cards">cards - 卡券表</option>
<option value="delivery_rules">delivery_rules - 发货规则表</option>
<option value="notification_channels">notification_channels - 通知渠道表</option>
<option value="user_settings">user_settings - 用户设置表</option>
<option value="system_settings">system_settings - 系统设置表</option>
<option value="email_verifications">email_verifications - 邮箱验证表</option>
<option value="captcha_codes">captcha_codes - 验证码表</option>
</select>
@ -254,14 +260,20 @@
const tableDescriptions = {
'users': '用户表',
'cookies': 'Cookie账号表',
'cookie_status': 'Cookie状态表',
'keywords': '关键字表',
'default_replies': '默认回复表',
'default_reply_records': '默认回复记录表',
'ai_reply_settings': 'AI回复设置表',
'ai_conversations': 'AI对话历史表',
'ai_item_cache': 'AI商品信息缓存表',
'item_info': '商品信息表',
'message_notifications': '消息通知表',
'cards': '卡券表',
'delivery_rules': '发货规则表',
'notification_channels': '通知渠道表',
'user_settings': '用户设置表',
'system_settings': '系统设置表',
'email_verifications': '邮箱验证表',
'captcha_codes': '验证码表'
};

View File

@ -314,16 +314,17 @@
</div>
<div class="content-body">
<!-- Cookie筛选 -->
<!-- 筛选和搜索 -->
<div class="card mb-4">
<div class="card-body">
<div class="row align-items-end">
<div class="row align-items-end mb-3">
<div class="col-md-6">
<label for="itemCookieFilter" class="form-label">筛选账号</label>
<select class="form-select" id="itemCookieFilter" onchange="loadItemsByCookie()">
<option value="">所有账号</option>
</select>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-end align-items-end gap-2">
<!-- 页码输入 -->
@ -345,18 +346,33 @@
</div>
</div>
</div>
<!-- 搜索结果统计 -->
<div id="itemSearchStats" class="text-muted small" style="display: none;">
<i class="bi bi-search me-1"></i>
<span id="itemSearchStatsText"></span>
</div>
</div>
</div>
<!-- 商品列表 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">商品列表(自动发货根据商品标题和商品详情匹配关键字)</h5>
<div class="d-flex align-items-center gap-3">
<input type="text" class="form-control" id="itemSearchInput"
placeholder="搜索商品标题或详情..." style="width: 300px;">
<h5 class="mb-0">商品列表(自动发货根据商品标题和商品详情匹配关键字)</h5>
</div>
<button class="btn btn-sm btn-outline-danger" onclick="batchDeleteItems()" id="batchDeleteBtn" disabled>
<i class="bi bi-trash"></i> 批量删除
</button>
</div>
<div class="card-body">
<!-- 搜索统计信息 -->
<div id="itemSearchStats" class="text-muted small mb-2" style="display: none;">
<i class="bi bi-search me-1"></i>
<span id="itemSearchStatsText"></span>
</div>
<div class="table-responsive">
<table class="table table-hover">
<thead>
@ -375,12 +391,54 @@
</thead>
<tbody id="itemsTableBody">
<tr>
<td colspan="7" class="text-center text-muted">加载中...</td>
<td colspan="8" class="text-center text-muted">加载中...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 分页控件 -->
<div class="card-footer" id="itemsPagination">
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted small">
<span id="itemsPageInfo">显示第 1-10 条,共 0 条记录</span>
</div>
<div class="d-flex align-items-center gap-2">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm" id="itemsFirstPage" onclick="goToItemsPage(1)" disabled>
<i class="bi bi-chevron-double-left"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="itemsPrevPage" onclick="goToItemsPage(currentItemsPage - 1)" disabled>
<i class="bi bi-chevron-left"></i>
</button>
</div>
<div class="d-flex align-items-center gap-2 mx-2">
<span class="text-muted small"></span>
<input type="number" class="form-control form-control-sm" id="itemsPageInput"
style="width: 60px;" min="1" value="1">
<span class="text-muted small">页,共 <span id="itemsTotalPages">0</span></span>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm" id="itemsNextPage" onclick="goToItemsPage(currentItemsPage + 1)" disabled>
<i class="bi bi-chevron-right"></i>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="itemsLastPage" onclick="goToItemsPage(totalItemsPages)" disabled>
<i class="bi bi-chevron-double-right"></i>
</button>
</div>
<select class="form-select form-select-sm ms-2" id="itemsPageSize" style="width: auto;">
<option value="10">10条/页</option>
<option value="20" selected>20条/页</option>
<option value="50">50条/页</option>
<option value="100">100条/页</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -17,6 +17,14 @@ let accountKeywordCache = {};
let cacheTimestamp = 0;
const CACHE_DURATION = 30000; // 30秒缓存
// 商品列表搜索和分页相关变量
let allItemsData = []; // 存储所有商品数据
let filteredItemsData = []; // 存储过滤后的商品数据
let currentItemsPage = 1; // 当前页码
let itemsPerPage = 20; // 每页显示数量
let totalItemsPages = 0; // 总页数
let currentSearchKeyword = ''; // 当前搜索关键词
// ================================
// 通用功能 - 菜单切换和导航
// ================================
@ -1793,6 +1801,9 @@ document.addEventListener('DOMContentLoaded', async () => {
// 初始化工具提示
initTooltips();
// 初始化商品搜索功能
initItemsSearch();
// 点击侧边栏外部关闭移动端菜单
document.addEventListener('click', function(e) {
const sidebar = document.getElementById('sidebar');
@ -4977,94 +4988,291 @@ async function loadItemsByCookie() {
// 显示商品列表
function displayItems(items) {
// 存储所有商品数据
allItemsData = items || [];
// 应用搜索过滤
applyItemsFilter();
// 显示当前页数据
displayCurrentPageItems();
// 更新分页控件
updateItemsPagination();
}
// 应用搜索过滤
function applyItemsFilter() {
const searchKeyword = currentSearchKeyword.toLowerCase().trim();
if (!searchKeyword) {
filteredItemsData = [...allItemsData];
} else {
filteredItemsData = allItemsData.filter(item => {
const title = (item.item_title || '').toLowerCase();
const detail = getItemDetailText(item.item_detail || '').toLowerCase();
return title.includes(searchKeyword) || detail.includes(searchKeyword);
});
}
// 重置到第一页
currentItemsPage = 1;
// 计算总页数
totalItemsPages = Math.ceil(filteredItemsData.length / itemsPerPage);
// 更新搜索统计
updateItemsSearchStats();
}
// 获取商品详情的纯文本内容
function getItemDetailText(itemDetail) {
if (!itemDetail) return '';
try {
// 尝试解析JSON
const detail = JSON.parse(itemDetail);
if (detail.content) {
return detail.content;
}
return itemDetail;
} catch (e) {
// 如果不是JSON格式直接返回原文本
return itemDetail;
}
}
// 显示当前页的商品数据
function displayCurrentPageItems() {
const tbody = document.getElementById('itemsTableBody');
if (!items || items.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无商品数据</td></tr>';
// 重置选择状态
const selectAllCheckbox = document.getElementById('selectAllItems');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
updateBatchDeleteButton();
return;
if (!filteredItemsData || filteredItemsData.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-muted">暂无商品数据</td></tr>';
resetItemsSelection();
return;
}
const itemsHtml = items.map(item => {
// 处理商品标题显示
let itemTitleDisplay = item.item_title || '未设置';
if (itemTitleDisplay.length > 30) {
itemTitleDisplay = itemTitleDisplay.substring(0, 30) + '...';
}
// 计算当前页的数据范围
const startIndex = (currentItemsPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentPageItems = filteredItemsData.slice(startIndex, endIndex);
// 处理商品详情显示
let itemDetailDisplay = '未设置';
if (item.item_detail) {
try {
// 尝试解析JSON并提取有用信息
const detail = JSON.parse(item.item_detail);
if (detail.content) {
itemDetailDisplay = detail.content.substring(0, 50) + (detail.content.length > 50 ? '...' : '');
} else {
// 如果是纯文本或其他格式直接显示前50个字符
itemDetailDisplay = item.item_detail.substring(0, 50) + (item.item_detail.length > 50 ? '...' : '');
const itemsHtml = currentPageItems.map(item => {
// 处理商品标题显示
let itemTitleDisplay = item.item_title || '未设置';
if (itemTitleDisplay.length > 30) {
itemTitleDisplay = itemTitleDisplay.substring(0, 30) + '...';
}
} catch (e) {
// 如果不是JSON格式直接显示前50个字符
itemDetailDisplay = item.item_detail.substring(0, 50) + (item.item_detail.length > 50 ? '...' : '');
// 处理商品详情显示
let itemDetailDisplay = '未设置';
if (item.item_detail) {
const detailText = getItemDetailText(item.item_detail);
itemDetailDisplay = detailText.substring(0, 50) + (detailText.length > 50 ? '...' : '');
}
}
// 多规格状态显示
const isMultiSpec = item.is_multi_spec;
const multiSpecDisplay = isMultiSpec ?
'<span class="badge bg-success">多规格</span>' :
'<span class="badge bg-secondary">普通</span>';
// 多规格状态显示
const isMultiSpec = item.is_multi_spec;
const multiSpecDisplay = isMultiSpec ?
'<span class="badge bg-success">多规格</span>' :
'<span class="badge bg-secondary">普通</span>';
return `
<tr>
<td>
<input type="checkbox" name="itemCheckbox"
data-cookie-id="${escapeHtml(item.cookie_id)}"
data-item-id="${escapeHtml(item.item_id)}"
onchange="updateSelectAllState()">
</td>
<td>${escapeHtml(item.cookie_id)}</td>
<td>${escapeHtml(item.item_id)}</td>
<td title="${escapeHtml(item.item_title || '未设置')}">${escapeHtml(itemTitleDisplay)}</td>
<td title="${escapeHtml(item.item_detail || '未设置')}">${escapeHtml(itemDetailDisplay)}</td>
<td>${multiSpecDisplay}</td>
<td>${formatDateTime(item.updated_at)}</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-primary" onclick="editItem('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}')" title="编辑详情">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}', '${escapeHtml(item.item_title || item.item_id)}')" title="删除">
<i class="bi bi-trash"></i>
</button>
<button class="btn btn-sm ${isMultiSpec ? 'btn-warning' : 'btn-success'}" onclick="toggleItemMultiSpec('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}', ${!isMultiSpec})" title="${isMultiSpec ? '关闭多规格' : '开启多规格'}">
<i class="bi ${isMultiSpec ? 'bi-toggle-on' : 'bi-toggle-off'}"></i>
</button>
</div>
</td>
</tr>
`;
return `
<tr>
<td>
<input type="checkbox" name="itemCheckbox"
data-cookie-id="${escapeHtml(item.cookie_id)}"
data-item-id="${escapeHtml(item.item_id)}"
onchange="updateSelectAllState()">
</td>
<td>${escapeHtml(item.cookie_id)}</td>
<td>${escapeHtml(item.item_id)}</td>
<td title="${escapeHtml(item.item_title || '未设置')}">${escapeHtml(itemTitleDisplay)}</td>
<td title="${escapeHtml(getItemDetailText(item.item_detail || ''))}">${escapeHtml(itemDetailDisplay)}</td>
<td>${multiSpecDisplay}</td>
<td>${formatDateTime(item.updated_at)}</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-primary" onclick="editItem('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}')" title="编辑详情">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}', '${escapeHtml(item.item_title || item.item_id)}')" title="删除">
<i class="bi bi-trash"></i>
</button>
<button class="btn btn-sm ${isMultiSpec ? 'btn-warning' : 'btn-success'}" onclick="toggleItemMultiSpec('${escapeHtml(item.cookie_id)}', '${escapeHtml(item.item_id)}', ${!isMultiSpec})" title="${isMultiSpec ? '关闭多规格' : '开启多规格'}">
<i class="bi ${isMultiSpec ? 'bi-toggle-on' : 'bi-toggle-off'}"></i>
</button>
</div>
</td>
</tr>
`;
}).join('');
// 更新表格内容
tbody.innerHTML = itemsHtml;
// 重置选择状态
resetItemsSelection();
}
// 重置商品选择状态
function resetItemsSelection() {
const selectAllCheckbox = document.getElementById('selectAllItems');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
}
updateBatchDeleteButton();
}
// 商品搜索过滤函数
function filterItems() {
const searchInput = document.getElementById('itemSearchInput');
currentSearchKeyword = searchInput ? searchInput.value : '';
// 应用过滤
applyItemsFilter();
// 显示当前页数据
displayCurrentPageItems();
// 更新分页控件
updateItemsPagination();
}
// 更新搜索统计信息
function updateItemsSearchStats() {
const statsElement = document.getElementById('itemSearchStats');
const statsTextElement = document.getElementById('itemSearchStatsText');
if (!statsElement || !statsTextElement) return;
if (currentSearchKeyword) {
statsTextElement.textContent = `搜索"${currentSearchKeyword}",找到 ${filteredItemsData.length} 个商品`;
statsElement.style.display = 'block';
} else {
statsElement.style.display = 'none';
}
}
// 更新分页控件
function updateItemsPagination() {
const paginationElement = document.getElementById('itemsPagination');
const pageInfoElement = document.getElementById('itemsPageInfo');
const totalPagesElement = document.getElementById('itemsTotalPages');
const pageInputElement = document.getElementById('itemsPageInput');
if (!paginationElement) return;
// 分页控件总是显示
paginationElement.style.display = 'block';
// 更新页面信息
const startIndex = (currentItemsPage - 1) * itemsPerPage + 1;
const endIndex = Math.min(currentItemsPage * itemsPerPage, filteredItemsData.length);
if (pageInfoElement) {
pageInfoElement.textContent = `显示第 ${startIndex}-${endIndex} 条,共 ${filteredItemsData.length} 条记录`;
}
if (totalPagesElement) {
totalPagesElement.textContent = totalItemsPages;
}
if (pageInputElement) {
pageInputElement.value = currentItemsPage;
pageInputElement.max = totalItemsPages;
}
// 更新分页按钮状态
updateItemsPaginationButtons();
}
// 更新分页按钮状态
function updateItemsPaginationButtons() {
const firstPageBtn = document.getElementById('itemsFirstPage');
const prevPageBtn = document.getElementById('itemsPrevPage');
const nextPageBtn = document.getElementById('itemsNextPage');
const lastPageBtn = document.getElementById('itemsLastPage');
if (firstPageBtn) firstPageBtn.disabled = currentItemsPage <= 1;
if (prevPageBtn) prevPageBtn.disabled = currentItemsPage <= 1;
if (nextPageBtn) nextPageBtn.disabled = currentItemsPage >= totalItemsPages;
if (lastPageBtn) lastPageBtn.disabled = currentItemsPage >= totalItemsPages;
}
// 跳转到指定页面
function goToItemsPage(page) {
if (page < 1 || page > totalItemsPages) return;
currentItemsPage = page;
displayCurrentPageItems();
updateItemsPagination();
}
// 处理页面输入框的回车事件
function handleItemsPageInput(event) {
if (event.key === 'Enter') {
const pageInput = event.target;
const page = parseInt(pageInput.value);
if (page >= 1 && page <= totalItemsPages) {
goToItemsPage(page);
} else {
pageInput.value = currentItemsPage;
}
}
}
// 改变每页显示数量
function changeItemsPageSize() {
const pageSizeSelect = document.getElementById('itemsPageSize');
if (!pageSizeSelect) return;
itemsPerPage = parseInt(pageSizeSelect.value);
// 重新计算总页数
totalItemsPages = Math.ceil(filteredItemsData.length / itemsPerPage);
// 调整当前页码,确保不超出范围
if (currentItemsPage > totalItemsPages) {
currentItemsPage = Math.max(1, totalItemsPages);
}
// 重新显示数据
displayCurrentPageItems();
updateItemsPagination();
}
// 初始化商品搜索功能
function initItemsSearch() {
// 初始化分页大小
const pageSizeSelect = document.getElementById('itemsPageSize');
if (pageSizeSelect) {
itemsPerPage = parseInt(pageSizeSelect.value) || 20;
pageSizeSelect.addEventListener('change', changeItemsPageSize);
}
// 初始化搜索输入框事件监听器
const searchInput = document.getElementById('itemSearchInput');
if (searchInput) {
// 使用防抖来避免频繁搜索
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
filterItems();
}, 300); // 300ms 防抖延迟
});
}
// 初始化页面输入框事件监听器
const pageInput = document.getElementById('itemsPageInput');
if (pageInput) {
pageInput.addEventListener('keydown', handleItemsPageInput);
}
}
// 刷新商品列表
async function refreshItems() {
await refreshItemsData();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 166 KiB