wenhongquan 7 месяцев назад
Родитель
Сommit
ac221c5ef1

+ 132 - 0
TEST_PAGE_README.md

@@ -0,0 +1,132 @@
+# Uperp 测试页面使用说明
+
+## 概述
+这个功能为 ERPNext 应用添加了一个测试页面,展示如何:
+- 创建自定义页面
+- 添加后端 API 接口
+- 实现前后端数据交互
+- 将页面添加到系统菜单
+
+## 文件结构
+
+### 后端文件
+- `uperp/uperp/api.py` - 后端 API 接口
+- `uperp/templates/pages/test.py` - 页面上下文处理
+- `uperp/config/desktop.py` - 桌面菜单配置
+- `uperp/config/uperp.py` - 模块菜单配置
+- `uperp/hooks.py` - 应用钩子配置
+
+### 前端文件
+- `uperp/templates/pages/test.html` - 页面HTML模板
+- `uperp/public/js/test-page.js` - 页面JavaScript逻辑
+- `uperp/public/css/test-page.css` - 页面样式
+
+## 功能特性
+
+### 后端 API
+1. `get_test_data()` - 获取测试数据列表
+2. `get_dashboard_stats()` - 获取仪表板统计数据
+3. `test_api_connection()` - 测试 API 连接
+4. `has_app_permission()` - 权限检查
+
+### 前端功能
+1. 响应式数据统计卡片
+2. 可交互的数据表格
+3. 数据刷新和导出功能
+4. API 连接测试
+5. 美观的用户界面
+
+## 访问方式
+
+### 1. 通过URL直接访问
+```
+http://your-erpnext-site/test
+```
+
+### 2. 通过系统菜单访问
+1. 登录 ERPNext 系统
+2. 在应用列表中找到 "Uperp" 应用
+3. 点击进入查看测试页面
+
+### 3. 通过模块菜单访问
+1. 进入 ERPNext 桌面
+2. 找到 "Uperp" 模块
+3. 在模块菜单中点击 "测试页面"
+
+## 安装和配置
+
+### 1. 安装应用
+```bash
+cd /path/to/your/bench
+bench get-app /path/to/uperp
+bench install-app uperp
+```
+
+### 2. 重启服务
+```bash
+bench restart
+```
+
+### 3. 清除缓存
+```bash
+bench clear-cache
+bench clear-website-cache
+```
+
+## 开发说明
+
+### 修改数据源
+在 `uperp/uperp/api.py` 中的 `get_test_data()` 函数可以修改数据源:
+
+```python
+# 从数据库获取真实数据
+test_data = frappe.get_all("YourDocType",
+                          fields=["name", "description", "status"],
+                          limit=10)
+```
+
+### 添加新的API接口
+在 `api.py` 文件中添加新的函数并使用 `@frappe.whitelist()` 装饰器:
+
+```python
+@frappe.whitelist()
+def your_new_api():
+    # 你的逻辑
+    return {"success": True, "data": "your_data"}
+```
+
+### 修改页面样式
+编辑 `uperp/public/css/test-page.css` 文件来调整页面样式。
+
+### 添加新功能
+在 `uperp/public/js/test-page.js` 中添加新的JavaScript函数。
+
+## 故障排除
+
+### 1. 页面无法访问
+- 检查应用是否正确安装
+- 确认 bench 服务正在运行
+- 清除缓存后重试
+
+### 2. API调用失败
+- 检查API函数是否添加了 `@frappe.whitelist()` 装饰器
+- 确认函数路径正确
+- 查看浏览器开发者工具的网络请求
+
+### 3. 样式问题
+- 确认CSS文件路径正确
+- 检查hooks.py中的样式文件配置
+- 清除浏览器缓存
+
+### 4. JavaScript错误
+- 打开浏览器开发者工具查看控制台错误
+- 确认JS文件路径正确
+- 检查frappe对象是否可用
+
+## 扩展建议
+
+1. 添加数据编辑功能
+2. 实现数据筛选和搜索
+3. 添加图表展示
+4. 实现实时数据更新
+5. 添加数据导入功能

+ 17 - 0
uperp/config/desktop.py

@@ -0,0 +1,17 @@
+from frappe import _
+
+
+def get_data():
+    """
+    返回桌面菜单数据
+    """
+    return [
+        {
+            "module_name": "Uperp",
+            "color": "grey",
+            "icon": "fa fa-cog",
+            "type": "module",
+            "label": _("Uperp"),
+            "description": _("Uperp应用管理"),
+        }
+    ]

+ 47 - 0
uperp/config/uperp.py

@@ -0,0 +1,47 @@
+from frappe import _
+
+
+def get_data():
+    """
+    返回Uperp模块的菜单项
+    """
+    return [
+        {
+            "label": _("测试功能"),
+            "items": [
+                {
+                    "type": "page",
+                    "name": "test",
+                    "label": _("测试页面"),
+                    "description": _("演示页面,展示数据请求和显示功能"),
+                    "route": "/test"
+                },
+                {
+                    "type": "report",
+                    "name": "Test Report",
+                    "label": _("测试报表"),
+                    "description": _("测试报表功能"),
+                    "is_query_report": True,
+                    "reference_doctype": "User"
+                }
+            ]
+        },
+        {
+            "label": _("系统管理"),
+            "items": [
+                {
+                    "type": "doctype",
+                    "name": "User",
+                    "label": _("用户管理"),
+                    "description": _("系统用户管理")
+                },
+                {
+                    "type": "page",
+                    "name": "test",
+                    "label": _("API测试"),
+                    "description": _("API接口测试页面"),
+                    "route": "/test"
+                }
+            ]
+        }
+    ]

+ 14 - 14
uperp/hooks.py

@@ -11,26 +11,26 @@ app_license = "mit"
 # required_apps = []
 
 # Each item in the list will be shown as an app in the apps page
-# add_to_apps_screen = [
-# 	{
-# 		"name": "uperp",
-# 		"logo": "/assets/uperp/logo.png",
-# 		"title": "Uperp",
-# 		"route": "/uperp",
-# 		"has_permission": "uperp.api.permission.has_app_permission"
-# 	}
-# ]
+add_to_apps_screen = [
+	{
+		"name": "uperp",
+		"logo": "/assets/uperp/logo.png",
+		"title": "Uperp",
+		"route": "/test",
+		"has_permission": "uperp.api.has_app_permission"
+	}
+]
 
 # Includes in <head>
 # ------------------
 
 # include js, css files in header of desk.html
-# app_include_css = "/assets/uperp/css/uperp.css"
-# app_include_js = "/assets/uperp/js/uperp.js"
+app_include_css = "/assets/uperp/css/test-page.css"
+app_include_js = "/assets/uperp/js/test-page.js"
 
 # include js, css files in header of web template
-# web_include_css = "/assets/uperp/css/uperp.css"
-# web_include_js = "/assets/uperp/js/uperp.js"
+web_include_css = "/assets/uperp/css/test-page.css"
+web_include_js = "/assets/uperp/js/test-page.js"
 
 # include custom scss in every website theme (without file extension ".scss")
 # website_theme_scss = "uperp/public/scss/website"
@@ -40,7 +40,7 @@ app_license = "mit"
 # webform_include_css = {"doctype": "public/css/doctype.css"}
 
 # include js in page
-# page_js = {"page" : "public/js/file.js"}
+page_js = {"test" : "public/js/test-page.js"}
 
 # include js in doctype views
 # doctype_js = {"doctype" : "public/js/doctype.js"}

+ 415 - 0
uperp/public/css/test-page.css

@@ -0,0 +1,415 @@
+/**
+ * 测试页面样式文件
+ * 提供页面的美观样式和布局
+ */
+
+/* 页面容器 */
+.test-page-container {
+    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+    line-height: 1.6;
+    color: #333;
+}
+
+/* 页面头部 */
+.page-header {
+    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+    color: white;
+    padding: 40px 0;
+    margin-bottom: 30px;
+}
+
+.page-title {
+    font-size: 2.5rem;
+    font-weight: 700;
+    margin-bottom: 10px;
+    text-shadow: 0 2px 4px rgba(0,0,0,0.3);
+}
+
+.page-subtitle {
+    font-size: 1.1rem;
+    opacity: 0.9;
+    margin-bottom: 0;
+}
+
+/* 主要内容区域 */
+.main-content {
+    padding-bottom: 40px;
+}
+
+/* 统计卡片区域 */
+.stats-section {
+    margin-bottom: 30px;
+}
+
+.stat-card {
+    background: white;
+    border-radius: 10px;
+    padding: 25px;
+    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+    border: 1px solid #e3e6f0;
+    transition: transform 0.3s ease, box-shadow 0.3s ease;
+    display: flex;
+    align-items: center;
+    margin-bottom: 20px;
+}
+
+.stat-card:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
+}
+
+.stat-icon {
+    width: 60px;
+    height: 60px;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-right: 20px;
+    font-size: 24px;
+}
+
+.stat-card:nth-child(1) .stat-icon {
+    background: linear-gradient(45deg, #4e73df, #224abe);
+    color: white;
+}
+
+.stat-card:nth-child(2) .stat-icon {
+    background: linear-gradient(45deg, #1cc88a, #13855c);
+    color: white;
+}
+
+.stat-card:nth-child(3) .stat-icon {
+    background: linear-gradient(45deg, #f6c23e, #dda20a);
+    color: white;
+}
+
+.stat-card:nth-child(4) .stat-icon {
+    background: linear-gradient(45deg, #e74a3b, #c0392b);
+    color: white;
+}
+
+.stat-content h3 {
+    font-size: 28px;
+    font-weight: 700;
+    margin-bottom: 5px;
+    color: #5a5c69;
+}
+
+.stat-content p {
+    font-size: 14px;
+    color: #858796;
+    margin-bottom: 0;
+    font-weight: 500;
+}
+
+/* 操作按钮区域 */
+.action-section {
+    margin-bottom: 30px;
+}
+
+.btn-group .btn {
+    margin-right: 10px;
+    border-radius: 25px;
+    font-weight: 500;
+    padding: 10px 20px;
+    transition: all 0.3s ease;
+}
+
+.btn-group .btn:hover {
+    transform: translateY(-1px);
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+}
+
+.btn-group .btn i {
+    margin-right: 5px;
+}
+
+/* 表格区域 */
+.table-section {
+    margin-bottom: 30px;
+}
+
+.card {
+    border: none;
+    border-radius: 10px;
+    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+    overflow: hidden;
+}
+
+.card-header {
+    background: linear-gradient(45deg, #f8f9fc, #eaecf4);
+    border-bottom: 1px solid #e3e6f0;
+    padding: 20px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.card-header h4 {
+    margin-bottom: 0;
+    color: #5a5c69;
+    font-weight: 600;
+}
+
+.loading-indicator {
+    color: #858796;
+    font-size: 14px;
+}
+
+.loading-indicator i {
+    margin-right: 5px;
+}
+
+.card-body {
+    padding: 0;
+}
+
+.table {
+    margin-bottom: 0;
+}
+
+.table thead th {
+    background-color: #f8f9fc;
+    border-bottom: 2px solid #e3e6f0;
+    color: #5a5c69;
+    font-weight: 600;
+    padding: 15px;
+    border-top: none;
+}
+
+.table tbody tr {
+    transition: background-color 0.3s ease;
+}
+
+.table tbody tr:hover {
+    background-color: #f8f9fc;
+}
+
+.table tbody td {
+    padding: 15px;
+    vertical-align: middle;
+    border-top: 1px solid #e3e6f0;
+}
+
+/* 状态徽章 */
+.badge {
+    font-size: 11px;
+    font-weight: 600;
+    padding: 5px 10px;
+    border-radius: 15px;
+}
+
+.badge-success {
+    background-color: #1cc88a;
+    color: white;
+}
+
+.badge-warning {
+    background-color: #f6c23e;
+    color: white;
+}
+
+.badge-danger {
+    background-color: #e74a3b;
+    color: white;
+}
+
+.badge-secondary {
+    background-color: #858796;
+    color: white;
+}
+
+/* 操作按钮 */
+.btn-sm {
+    padding: 5px 10px;
+    font-size: 12px;
+    border-radius: 15px;
+    margin-right: 5px;
+    transition: all 0.3s ease;
+}
+
+.btn-sm:hover {
+    transform: translateY(-1px);
+}
+
+/* 消息区域 */
+.message-section {
+    position: fixed;
+    top: 20px;
+    right: 20px;
+    z-index: 1050;
+    max-width: 400px;
+}
+
+.alert {
+    border: none;
+    border-radius: 10px;
+    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+    font-weight: 500;
+}
+
+.alert-success {
+    background-color: #d4edda;
+    color: #155724;
+    border-left: 4px solid #1cc88a;
+}
+
+.alert-danger {
+    background-color: #f8d7da;
+    color: #721c24;
+    border-left: 4px solid #e74a3b;
+}
+
+.alert-warning {
+    background-color: #fff3cd;
+    color: #856404;
+    border-left: 4px solid #f6c23e;
+}
+
+.alert-info {
+    background-color: #d1ecf1;
+    color: #0c5460;
+    border-left: 4px solid #36b9cc;
+}
+
+/* 模态框样式 */
+.modal-content {
+    border: none;
+    border-radius: 10px;
+    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
+}
+
+.modal-header {
+    background: linear-gradient(45deg, #667eea, #764ba2);
+    color: white;
+    border-bottom: none;
+    border-radius: 10px 10px 0 0;
+}
+
+.modal-header .btn-close {
+    filter: invert(1);
+}
+
+.modal-body .table th {
+    background-color: #f8f9fc;
+    border: none;
+    font-weight: 600;
+    color: #5a5c69;
+    width: 120px;
+}
+
+.modal-body .table td {
+    border: none;
+    color: #858796;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+    .page-title {
+        font-size: 2rem;
+    }
+
+    .page-subtitle {
+        font-size: 1rem;
+    }
+
+    .stat-card {
+        margin-bottom: 15px;
+        padding: 20px;
+    }
+
+    .stat-icon {
+        width: 50px;
+        height: 50px;
+        font-size: 20px;
+        margin-right: 15px;
+    }
+
+    .stat-content h3 {
+        font-size: 24px;
+    }
+
+    .btn-group .btn {
+        margin-bottom: 10px;
+        margin-right: 5px;
+        padding: 8px 15px;
+    }
+
+    .table-responsive {
+        font-size: 14px;
+    }
+
+    .message-section {
+        position: relative;
+        top: auto;
+        right: auto;
+        max-width: 100%;
+        margin-top: 20px;
+    }
+}
+
+@media (max-width: 576px) {
+    .page-header {
+        padding: 30px 0;
+    }
+
+    .stat-card {
+        flex-direction: column;
+        text-align: center;
+    }
+
+    .stat-icon {
+        margin-right: 0;
+        margin-bottom: 15px;
+    }
+
+    .card-header {
+        flex-direction: column;
+        text-align: center;
+    }
+
+    .card-header h4 {
+        margin-bottom: 10px;
+    }
+}
+
+/* 动画效果 */
+@keyframes fadeIn {
+    from {
+        opacity: 0;
+        transform: translateY(20px);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.test-page-container {
+    animation: fadeIn 0.5s ease-out;
+}
+
+/* 表格行动画 */
+.table tbody tr {
+    animation: fadeIn 0.3s ease-out;
+}
+
+/* 滚动条美化 */
+::-webkit-scrollbar {
+    width: 8px;
+}
+
+::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+}

+ 307 - 0
uperp/public/js/test-page.js

@@ -0,0 +1,307 @@
+/**
+ * 测试页面的JavaScript文件
+ * 处理数据请求和用户交互
+ */
+
+// 全局变量
+let testData = [];
+let statsData = {};
+
+/**
+ * 初始化测试页面
+ */
+function initTestPage() {
+    console.log('初始化测试页面...');
+
+    // 加载数据
+    loadTestData();
+    loadStatsData();
+
+    // 绑定事件
+    bindEvents();
+}
+
+/**
+ * 绑定页面事件
+ */
+function bindEvents() {
+    // 可以在这里添加更多的事件绑定
+    console.log('事件绑定完成');
+}
+
+/**
+ * 加载测试数据
+ */
+function loadTestData() {
+    showLoading(true);
+
+    frappe.call({
+        method: 'uperp.uperp.api.get_test_data',
+        callback: function(response) {
+            hideLoading();
+
+            if (response.message && response.message.success) {
+                testData = response.message.data;
+                renderTestDataTable();
+                showMessage('数据加载成功', 'success');
+            } else {
+                const errorMsg = response.message ? response.message.message : '数据加载失败';
+                showMessage(errorMsg, 'error');
+                console.error('加载测试数据失败:', response);
+            }
+        },
+        error: function(error) {
+            hideLoading();
+            showMessage('网络请求失败', 'error');
+            console.error('API请求错误:', error);
+        }
+    });
+}
+
+/**
+ * 加载统计数据
+ */
+function loadStatsData() {
+    frappe.call({
+        method: 'uperp.uperp.api.get_dashboard_stats',
+        callback: function(response) {
+            if (response.message && response.message.success) {
+                statsData = response.message.stats;
+                renderStatsCards();
+            } else {
+                console.error('加载统计数据失败:', response);
+            }
+        },
+        error: function(error) {
+            console.error('统计数据API请求错误:', error);
+        }
+    });
+}
+
+/**
+ * 渲染数据表格
+ */
+function renderTestDataTable() {
+    const tbody = document.getElementById('test-data-tbody');
+    if (!tbody) return;
+
+    // 清空现有内容
+    tbody.innerHTML = '';
+
+    if (!testData || testData.length === 0) {
+        tbody.innerHTML = '<tr><td colspan="7" class="text-center">暂无数据</td></tr>';
+        return;
+    }
+
+    // 生成表格行
+    testData.forEach(function(item) {
+        const row = document.createElement('tr');
+        row.innerHTML = `
+            <td>${item.id}</td>
+            <td>${item.name}</td>
+            <td>${item.description}</td>
+            <td>
+                <span class="badge ${getStatusBadgeClass(item.status)}">
+                    ${item.status}
+                </span>
+            </td>
+            <td>${item.created_date}</td>
+            <td>${item.value}</td>
+            <td>
+                <button class="btn btn-sm btn-primary" onclick="viewDetails(${item.id})">
+                    <i class="fa fa-eye"></i> 查看
+                </button>
+                <button class="btn btn-sm btn-warning" onclick="editItem(${item.id})">
+                    <i class="fa fa-edit"></i> 编辑
+                </button>
+            </td>
+        `;
+        tbody.appendChild(row);
+    });
+}
+
+/**
+ * 渲染统计卡片
+ */
+function renderStatsCards() {
+    // 更新统计数字
+    updateElementText('total-records', statsData.total_records || 0);
+    updateElementText('active-records', statsData.active_records || 0);
+    updateElementText('pending-records', statsData.pending_records || 0);
+    updateElementText('total-value', statsData.total_value || 0);
+}
+
+/**
+ * 获取状态徽章样式类
+ */
+function getStatusBadgeClass(status) {
+    switch (status) {
+        case '激活':
+            return 'badge-success';
+        case '待审核':
+            return 'badge-warning';
+        case '停用':
+            return 'badge-danger';
+        default:
+            return 'badge-secondary';
+    }
+}
+
+/**
+ * 更新元素文本内容
+ */
+function updateElementText(elementId, text) {
+    const element = document.getElementById(elementId);
+    if (element) {
+        element.textContent = text;
+    }
+}
+
+/**
+ * 显示/隐藏加载指示器
+ */
+function showLoading(show = true) {
+    const indicator = document.getElementById('loading-indicator');
+    if (indicator) {
+        indicator.style.display = show ? 'block' : 'none';
+    }
+}
+
+function hideLoading() {
+    showLoading(false);
+}
+
+/**
+ * 显示消息
+ */
+function showMessage(message, type = 'info') {
+    const messageArea = document.getElementById('message-area');
+    if (!messageArea) return;
+
+    const alertClass = `alert-${type === 'error' ? 'danger' : type}`;
+    messageArea.innerHTML = `
+        <div class="alert ${alertClass} alert-dismissible fade show" role="alert">
+            ${message}
+            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+        </div>
+    `;
+    messageArea.style.display = 'block';
+
+    // 自动隐藏消息(3秒后)
+    setTimeout(function() {
+        const alert = messageArea.querySelector('.alert');
+        if (alert) {
+            alert.remove();
+        }
+        if (messageArea.children.length === 0) {
+            messageArea.style.display = 'none';
+        }
+    }, 3000);
+}
+
+/**
+ * 测试API连接
+ */
+function testApiConnection() {
+    frappe.call({
+        method: 'uperp.uperp.api.test_api_connection',
+        callback: function(response) {
+            if (response.message && response.message.success) {
+                showMessage(`API连接正常! 当前用户: ${response.message.user}`, 'success');
+            } else {
+                showMessage('API连接测试失败', 'error');
+            }
+        },
+        error: function(error) {
+            showMessage('API连接测试失败', 'error');
+            console.error('API连接测试错误:', error);
+        }
+    });
+}
+
+/**
+ * 导出数据
+ */
+function exportData() {
+    if (!testData || testData.length === 0) {
+        showMessage('没有可导出的数据', 'warning');
+        return;
+    }
+
+    // 创建CSV内容
+    let csvContent = "ID,名称,描述,状态,创建日期,数值\n";
+    testData.forEach(function(item) {
+        csvContent += `${item.id},"${item.name}","${item.description}","${item.status}","${item.created_date}",${item.value}\n`;
+    });
+
+    // 创建下载链接
+    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+    const link = document.createElement('a');
+    const url = URL.createObjectURL(blob);
+    link.setAttribute('href', url);
+    link.setAttribute('download', 'test-data.csv');
+    link.style.visibility = 'hidden';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+
+    showMessage('数据导出成功', 'success');
+}
+
+/**
+ * 查看详情
+ */
+function viewDetails(id) {
+    const item = testData.find(data => data.id === id);
+    if (item) {
+        const detailsHtml = `
+            <div class="modal fade" id="detailsModal" tabindex="-1">
+                <div class="modal-dialog">
+                    <div class="modal-content">
+                        <div class="modal-header">
+                            <h5 class="modal-title">详情信息</h5>
+                            <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
+                        </div>
+                        <div class="modal-body">
+                            <table class="table">
+                                <tr><th>ID:</th><td>${item.id}</td></tr>
+                                <tr><th>名称:</th><td>${item.name}</td></tr>
+                                <tr><th>描述:</th><td>${item.description}</td></tr>
+                                <tr><th>状态:</th><td>${item.status}</td></tr>
+                                <tr><th>创建日期:</th><td>${item.created_date}</td></tr>
+                                <tr><th>数值:</th><td>${item.value}</td></tr>
+                            </table>
+                        </div>
+                        <div class="modal-footer">
+                            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        `;
+
+        // 移除旧的模态框
+        const oldModal = document.getElementById('detailsModal');
+        if (oldModal) {
+            oldModal.remove();
+        }
+
+        // 添加新的模态框
+        document.body.insertAdjacentHTML('beforeend', detailsHtml);
+
+        // 显示模态框
+        const modal = new bootstrap.Modal(document.getElementById('detailsModal'));
+        modal.show();
+    }
+}
+
+/**
+ * 编辑项目
+ */
+function editItem(id) {
+    const item = testData.find(data => data.id === id);
+    if (item) {
+        // 这里可以实现编辑功能
+        showMessage(`编辑功能待实现 - ID: ${id}`, 'info');
+    }
+}

+ 136 - 0
uperp/templates/pages/test.html

@@ -0,0 +1,136 @@
+{% extends "templates/web.html" %}
+
+{% block page_content %}
+<div class="test-page-container">
+    <!-- 页面标题 -->
+    <div class="page-header">
+        <div class="container">
+            <h1 class="page-title">测试页面</h1>
+            <p class="page-subtitle">这是一个演示页面,展示如何在ERPNext应用中添加自定义页面</p>
+        </div>
+    </div>
+
+    <!-- 主要内容区域 -->
+    <div class="container main-content">
+        <!-- 统计卡片区域 -->
+        <div class="row stats-section">
+            <div class="col-md-3">
+                <div class="stat-card">
+                    <div class="stat-icon">
+                        <i class="fa fa-database"></i>
+                    </div>
+                    <div class="stat-content">
+                        <h3 id="total-records">-</h3>
+                        <p>总记录数</p>
+                    </div>
+                </div>
+            </div>
+            <div class="col-md-3">
+                <div class="stat-card">
+                    <div class="stat-icon">
+                        <i class="fa fa-check-circle"></i>
+                    </div>
+                    <div class="stat-content">
+                        <h3 id="active-records">-</h3>
+                        <p>激活记录</p>
+                    </div>
+                </div>
+            </div>
+            <div class="col-md-3">
+                <div class="stat-card">
+                    <div class="stat-icon">
+                        <i class="fa fa-clock-o"></i>
+                    </div>
+                    <div class="stat-content">
+                        <h3 id="pending-records">-</h3>
+                        <p>待处理记录</p>
+                    </div>
+                </div>
+            </div>
+            <div class="col-md-3">
+                <div class="stat-card">
+                    <div class="stat-icon">
+                        <i class="fa fa-money"></i>
+                    </div>
+                    <div class="stat-content">
+                        <h3 id="total-value">-</h3>
+                        <p>总价值</p>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 操作按钮区域 -->
+        <div class="row action-section">
+            <div class="col-md-12">
+                <div class="btn-group" role="group">
+                    <button type="button" class="btn btn-primary" onclick="loadTestData()">
+                        <i class="fa fa-refresh"></i> 刷新数据
+                    </button>
+                    <button type="button" class="btn btn-info" onclick="testApiConnection()">
+                        <i class="fa fa-plug"></i> 测试连接
+                    </button>
+                    <button type="button" class="btn btn-success" onclick="exportData()">
+                        <i class="fa fa-download"></i> 导出数据
+                    </button>
+                </div>
+            </div>
+        </div>
+
+        <!-- 数据表格区域 -->
+        <div class="row table-section">
+            <div class="col-md-12">
+                <div class="card">
+                    <div class="card-header">
+                        <h4>测试数据列表</h4>
+                        <div class="loading-indicator" id="loading-indicator" style="display: none;">
+                            <i class="fa fa-spinner fa-spin"></i> 加载中...
+                        </div>
+                    </div>
+                    <div class="card-body">
+                        <div class="table-responsive">
+                            <table class="table table-striped table-hover" id="test-data-table">
+                                <thead>
+                                    <tr>
+                                        <th>ID</th>
+                                        <th>名称</th>
+                                        <th>描述</th>
+                                        <th>状态</th>
+                                        <th>创建日期</th>
+                                        <th>数值</th>
+                                        <th>操作</th>
+                                    </tr>
+                                </thead>
+                                <tbody id="test-data-tbody">
+                                    <!-- 数据将通过JavaScript动态填充 -->
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 消息提示区域 -->
+        <div class="row message-section">
+            <div class="col-md-12">
+                <div id="message-area" style="display: none;">
+                    <!-- 消息将通过JavaScript动态显示 -->
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!-- 包含JavaScript和CSS文件 -->
+<link rel="stylesheet" href="/assets/uperp/css/test-page.css">
+<script src="/assets/uperp/js/test-page.js"></script>
+
+<!-- 页面初始化脚本 -->
+<script>
+document.addEventListener('DOMContentLoaded', function() {
+    // 页面加载完成后初始化
+    initTestPage();
+});
+</script>
+{% endblock %}

+ 19 - 0
uperp/templates/pages/test.py

@@ -0,0 +1,19 @@
+import frappe
+from frappe import _
+
+
+def get_context(context):
+    """
+    为测试页面提供上下文数据
+    """
+    context.title = _("测试页面")
+    context.show_sidebar = False
+
+    # 可以在这里添加页面需要的初始数据
+    context.page_data = {
+        "app_name": "uperp",
+        "page_title": _("测试页面"),
+        "description": _("这是一个演示页面,展示如何在ERPNext应用中添加自定义页面")
+    }
+
+    return context

+ 108 - 0
uperp/uperp/api.py

@@ -0,0 +1,108 @@
+import frappe
+from frappe import _
+
+
+@frappe.whitelist()
+def has_app_permission():
+    """
+    检查用户是否有权限访问应用
+    """
+    # 这里可以添加具体的权限检查逻辑
+    # 目前允许所有登录用户访问
+    return True
+
+
+@frappe.whitelist()
+def get_test_data():
+    """
+    测试页面的数据接口
+    返回测试数据给前端展示
+    """
+    try:
+        # 模拟从数据库获取数据,这里返回一些测试数据
+        test_data = [
+            {
+                "id": 1,
+                "name": "用户测试数据 1",
+                "description": "这是第一条测试数据",
+                "status": "激活",
+                "created_date": "2024-01-01",
+                "value": 100
+            },
+            {
+                "id": 2,
+                "name": "用户测试数据 2",
+                "description": "这是第二条测试数据",
+                "status": "待审核",
+                "created_date": "2024-01-02",
+                "value": 200
+            },
+            {
+                "id": 3,
+                "name": "用户测试数据 3",
+                "description": "这是第三条测试数据",
+                "status": "激活",
+                "created_date": "2024-01-03",
+                "value": 300
+            }
+        ]
+
+        # 也可以从实际数据库查询数据,例如:
+        # test_data = frappe.get_all("DocType Name",
+        #                          fields=["name", "description", "status"],
+        #                          limit=10)
+
+        return {
+            "success": True,
+            "data": test_data,
+            "message": _("数据获取成功")
+        }
+
+    except Exception as e:
+        frappe.log_error(frappe.get_traceback(), "获取测试数据失败")
+        return {
+            "success": False,
+            "data": [],
+            "message": _("获取数据失败: {0}").format(str(e))
+        }
+
+
+@frappe.whitelist()
+def get_dashboard_stats():
+    """
+    获取仪表板统计数据
+    """
+    try:
+        stats = {
+            "total_records": 3,
+            "active_records": 2,
+            "pending_records": 1,
+            "total_value": 600
+        }
+
+        return {
+            "success": True,
+            "stats": stats,
+            "message": _("统计数据获取成功")
+        }
+
+    except Exception as e:
+        frappe.log_error(frappe.get_traceback(), "获取统计数据失败")
+        return {
+            "success": False,
+            "stats": {},
+            "message": _("获取统计数据失败: {0}").format(str(e))
+        }
+
+
+@frappe.whitelist()
+def test_api_connection():
+    """
+    测试 API 连接
+    """
+    return {
+        "success": True,
+        "message": _("API 连接正常"),
+        "timestamp": frappe.utils.now(),
+        "user": frappe.session.user
+    }