dab6140f99
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>
289 lines
13 KiB
JavaScript
289 lines
13 KiB
JavaScript
#!/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);
|
||
});
|