feat: Page Data dev loop, adm Code Run UI, and customer order demo
Add verify→Aider→OpenHands dev loop, memindadm config/history UI, and john4 customer-order Page Data demo with scenario + verification scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* john4 客户下单系统(Page Data 前后台)演示搭建。
|
||||
* Usage: node scripts/setup-customer-order-demo.mjs
|
||||
*/
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { loadH5Environment } from './load-env.mjs';
|
||||
import { createDbPool } from '../db.mjs';
|
||||
import { bindWorkspaceHtmlForPageData } from '../page-data-workspace-bind.mjs';
|
||||
import { resolveMindSpaceStorageRoot } from '../mindspace-runtime-config.mjs';
|
||||
import { createUserDataSpaceService } from '../user-data-space-service.mjs';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
loadH5Environment(import.meta.dirname);
|
||||
|
||||
const USER_ID = process.env.CUSTOMER_ORDER_USER_ID ?? '32035858-9a20-425b-89da-c118ef0779aa';
|
||||
const WORKSPACE_ROOT = path.join(root, 'MindSpace', USER_ID);
|
||||
const ADMIN_PASSWORD = '88888888';
|
||||
const DATASET = 'customer_orders';
|
||||
|
||||
const ORDER_FORM_HTML = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>客户下单</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: linear-gradient(135deg,#0f766e,#115e59); min-height: 100vh; padding: 32px 16px; }
|
||||
.card { max-width: 640px; margin: 0 auto; background: #fff; border-radius: 16px; padding: 28px; box-shadow: 0 20px 60px rgba(0,0,0,.18); }
|
||||
h1 { text-align: center; margin-bottom: 8px; color: #134e4a; }
|
||||
p.sub { text-align: center; color: #64748b; margin-bottom: 24px; font-size: 14px; }
|
||||
label { display: block; font-weight: 600; margin: 14px 0 6px; color: #334155; }
|
||||
input, textarea { width: 100%; padding: 12px 14px; border: 1px solid #cbd5e1; border-radius: 10px; font: inherit; }
|
||||
textarea { min-height: 80px; resize: vertical; }
|
||||
button { width: 100%; margin-top: 20px; padding: 14px; border: 0; border-radius: 12px; background: #0f766e; color: #fff; font-size: 16px; font-weight: 700; cursor: pointer; }
|
||||
button:disabled { opacity: .6; cursor: not-allowed; }
|
||||
.msg { margin-top: 16px; padding: 12px; border-radius: 10px; display: none; }
|
||||
.msg.ok { display: block; background: #ecfdf5; color: #047857; }
|
||||
.msg.err { display: block; background: #fef2f2; color: #b91c1c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>客户下单</h1>
|
||||
<p class="sub">请填写订单信息,提交后将保存到 Page Data。</p>
|
||||
<form id="orderForm">
|
||||
<label>客户姓名 *</label><input id="customer_name" required>
|
||||
<label>联系电话 *</label><input id="phone" required>
|
||||
<label>商品名称 *</label><input id="product_name" required>
|
||||
<label>购买数量 *</label><input id="quantity" type="number" min="1" value="1" required>
|
||||
<label>收货地址 *</label><textarea id="address" required></textarea>
|
||||
<label>备注</label><textarea id="remark"></textarea>
|
||||
<button type="submit" id="submitBtn">提交订单</button>
|
||||
</form>
|
||||
<div class="msg" id="message"></div>
|
||||
</div>
|
||||
<script src="/assets/page-data-client.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var client = MindSpacePageData.createClient({ apiBase: '/api' });
|
||||
var form = document.getElementById('orderForm');
|
||||
var message = document.getElementById('message');
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
message.className = 'msg';
|
||||
message.style.display = 'none';
|
||||
var payload = {
|
||||
customer_name: document.getElementById('customer_name').value.trim(),
|
||||
phone: document.getElementById('phone').value.trim(),
|
||||
product_name: document.getElementById('product_name').value.trim(),
|
||||
quantity: String(document.getElementById('quantity').value || '1'),
|
||||
address: document.getElementById('address').value.trim(),
|
||||
remark: document.getElementById('remark').value.trim(),
|
||||
status: '待处理'
|
||||
};
|
||||
if (!payload.customer_name || !payload.phone || !payload.product_name || !payload.address) {
|
||||
message.className = 'msg err'; message.textContent = '请填写所有必填项'; message.style.display = 'block'; return;
|
||||
}
|
||||
document.getElementById('submitBtn').disabled = true;
|
||||
try {
|
||||
await client.insertRow('customer_orders', payload);
|
||||
message.className = 'msg ok'; message.textContent = '下单成功!';
|
||||
message.style.display = 'block';
|
||||
form.reset();
|
||||
document.getElementById('quantity').value = '1';
|
||||
} catch (err) {
|
||||
message.className = 'msg err'; message.textContent = '提交失败:' + (err.message || err);
|
||||
message.style.display = 'block';
|
||||
} finally {
|
||||
document.getElementById('submitBtn').disabled = false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const ORDER_ADMIN_HTML = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>订单管理后台</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f1f5f9; min-height: 100vh; padding: 24px 16px; }
|
||||
.wrap { max-width: 1100px; margin: 0 auto; }
|
||||
.hero { background: #0f766e; color: #fff; border-radius: 14px; padding: 24px; margin-bottom: 16px; }
|
||||
.panel { background: #fff; border-radius: 14px; padding: 24px; box-shadow: 0 8px 24px rgba(15,23,42,.06); }
|
||||
input, button { font: inherit; }
|
||||
.auth input { width: 100%; max-width: 280px; padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: 8px; margin-right: 8px; }
|
||||
.auth button, .toolbar button { padding: 10px 16px; border: 0; border-radius: 8px; background: #0f766e; color: #fff; cursor: pointer; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 16px; font-size: 14px; }
|
||||
th, td { padding: 10px 8px; border-bottom: 1px solid #e2e8f0; text-align: left; vertical-align: top; }
|
||||
th { background: #f8fafc; }
|
||||
.stats { display: flex; gap: 12px; flex-wrap: wrap; margin: 12px 0; }
|
||||
.stat { background: #ecfdf5; color: #065f46; padding: 10px 14px; border-radius: 10px; font-size: 13px; }
|
||||
.hidden { display: none; }
|
||||
.err { color: #b91c1c; margin-top: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="hero"><h1>客户订单管理后台</h1><p>查看订单、按商品统计、导出 CSV</p></div>
|
||||
<div class="panel auth" id="authBox">
|
||||
<h2 style="margin-bottom:12px">请输入管理口令</h2>
|
||||
<input type="password" id="passwordInput" placeholder="管理口令">
|
||||
<button id="authBtn">进入后台</button>
|
||||
<div class="err hidden" id="authError"></div>
|
||||
</div>
|
||||
<div class="panel hidden" id="dash">
|
||||
<div class="toolbar">
|
||||
<button id="refreshBtn">刷新</button>
|
||||
<button id="exportBtn">导出 CSV</button>
|
||||
</div>
|
||||
<div class="stats" id="stats"></div>
|
||||
<table>
|
||||
<thead><tr><th>#</th><th>客户</th><th>电话</th><th>商品</th><th>数量</th><th>地址</th><th>状态</th><th>时间</th></tr></thead>
|
||||
<tbody id="tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/page-data-client.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
var client = MindSpacePageData.createClient({ apiBase: '/api' });
|
||||
var rowsCache = [];
|
||||
document.getElementById('authBtn').addEventListener('click', async function () {
|
||||
try {
|
||||
await client.authenticate(document.getElementById('passwordInput').value.trim());
|
||||
document.getElementById('authBox').classList.add('hidden');
|
||||
document.getElementById('dash').classList.remove('hidden');
|
||||
await loadRows();
|
||||
} catch (e) {
|
||||
var err = document.getElementById('authError');
|
||||
err.textContent = '口令错误';
|
||||
err.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadRows);
|
||||
document.getElementById('exportBtn').addEventListener('click', exportCsv);
|
||||
async function loadRows() {
|
||||
var result = await client.listRows('customer_orders', { limit: 200 });
|
||||
rowsCache = result.rows || [];
|
||||
renderStats(rowsCache);
|
||||
renderTable(rowsCache);
|
||||
}
|
||||
function renderStats(rows) {
|
||||
var counts = {};
|
||||
rows.forEach(function (r) { counts[r.product_name] = (counts[r.product_name] || 0) + 1; });
|
||||
document.getElementById('stats').innerHTML = Object.keys(counts).map(function (k) {
|
||||
return '<div class="stat">' + k + ':' + counts[k] + ' 单</div>';
|
||||
}).join('') || '<div class="stat">暂无订单</div>';
|
||||
}
|
||||
function renderTable(rows) {
|
||||
var tbody = document.getElementById('tbody');
|
||||
if (!rows.length) { tbody.innerHTML = '<tr><td colspan="8">暂无数据</td></tr>'; return; }
|
||||
tbody.innerHTML = rows.map(function (r, i) {
|
||||
return '<tr><td>' + (i+1) + '</td><td>' + esc(r.customer_name) + '</td><td>' + esc(r.phone) +
|
||||
'</td><td>' + esc(r.product_name) + '</td><td>' + esc(r.quantity) + '</td><td>' + esc(r.address) +
|
||||
'</td><td>' + esc(r.status) + '</td><td>' + esc(r.created_at || '') + '</td></tr>';
|
||||
}).join('');
|
||||
}
|
||||
function exportCsv() {
|
||||
var header = ['customer_name','phone','product_name','quantity','address','remark','status','created_at'];
|
||||
var lines = [header.join(',')].concat(rowsCache.map(function (r) {
|
||||
return header.map(function (k) { return '"' + String(r[k] ?? '').replace(/"/g, '""') + '"'; }).join(',');
|
||||
}));
|
||||
var blob = new Blob([lines.join('\\n')], { type: 'text/csv;charset=utf-8' });
|
||||
var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'customer-orders.csv'; a.click();
|
||||
}
|
||||
function esc(v) { return String(v ?? '').replace(/[&<>"]/g, function (c) { return ({'&':'&','<':'<','>':'>','"':'"'}[c]); }); }
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const FORM_POLICY = {
|
||||
accessMode: 'public',
|
||||
datasets: {
|
||||
customer_orders: {
|
||||
insert: true,
|
||||
read: false,
|
||||
columns: {
|
||||
insert: ['customer_name', 'phone', 'product_name', 'quantity', 'address', 'remark', 'status'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ADMIN_POLICY = {
|
||||
accessMode: 'password',
|
||||
datasets: {
|
||||
customer_orders: {
|
||||
insert: false,
|
||||
read: true,
|
||||
columns: {
|
||||
read: ['id', 'customer_name', 'phone', 'product_name', 'quantity', 'address', 'remark', 'status', 'created_at'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const publicDir = path.join(WORKSPACE_ROOT, 'public');
|
||||
await fs.mkdir(publicDir, { recursive: true });
|
||||
await fs.writeFile(path.join(publicDir, 'customer-order-form.html'), ORDER_FORM_HTML, 'utf8');
|
||||
await fs.writeFile(path.join(publicDir, 'customer-order-admin.html'), ORDER_ADMIN_HTML, 'utf8');
|
||||
|
||||
const dataSpace = createUserDataSpaceService({ workspaceRoot: WORKSPACE_ROOT, userId: USER_ID });
|
||||
await dataSpace.executeSql(`CREATE TABLE IF NOT EXISTS customer_orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
customer_name TEXT NOT NULL DEFAULT '',
|
||||
phone TEXT NOT NULL DEFAULT '',
|
||||
product_name TEXT NOT NULL DEFAULT '',
|
||||
quantity TEXT NOT NULL DEFAULT '1',
|
||||
address TEXT NOT NULL DEFAULT '',
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT '待处理',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', '+8 hours'))
|
||||
);`);
|
||||
await dataSpace.upsertDataset({
|
||||
name: DATASET,
|
||||
table: DATASET,
|
||||
description: '客户下单系统订单表',
|
||||
actions: ['read', 'insert'],
|
||||
columns: {
|
||||
insert: FORM_POLICY.datasets.customer_orders.columns.insert,
|
||||
read: ADMIN_POLICY.datasets.customer_orders.columns.read,
|
||||
},
|
||||
});
|
||||
|
||||
const pool = createDbPool();
|
||||
const storageRoot = resolveMindSpaceStorageRoot(root);
|
||||
const pages = [
|
||||
{ relativePath: 'public/customer-order-form.html', accessMode: 'public', password: null, policy: FORM_POLICY },
|
||||
{ relativePath: 'public/customer-order-admin.html', accessMode: 'password', password: ADMIN_PASSWORD, policy: ADMIN_POLICY },
|
||||
];
|
||||
|
||||
console.log('==> 客户下单系统 Page Data 演示\n');
|
||||
for (const page of pages) {
|
||||
const result = await bindWorkspaceHtmlForPageData({
|
||||
pool,
|
||||
h5Root: root,
|
||||
storageRoot,
|
||||
userId: USER_ID,
|
||||
workspaceRoot: WORKSPACE_ROOT,
|
||||
relativePath: page.relativePath,
|
||||
accessMode: page.accessMode,
|
||||
password: page.password,
|
||||
pageDataPolicy: page.policy,
|
||||
});
|
||||
console.log(`✓ ${page.relativePath}`);
|
||||
console.log(` pageId: ${result.pageId}`);
|
||||
console.log(` URL: ${result.workspaceUrl}\n`);
|
||||
}
|
||||
|
||||
console.log('后台口令:', ADMIN_PASSWORD);
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err instanceof Error ? err.stack ?? err.message : err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user