Files
memind/scripts/setup-customer-order-demo.mjs
T
john dab6140f99 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>
2026-07-23 22:11:18 +08:00

289 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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);
});