产品迭代模块:卡片改表格 + 日期内联编辑 + 后端日期校验
- 卡片列表改为表格列表(10列),参考用户运营中心产品台账 - 数据库新增 priority + 5 个日期字段(start/plan/dev_done/test/launch) - 删除 owner/platform/feature_list 字段(migrate_drop_product_fields) - 日期内联编辑:5个日期列直接渲染 date input - 后端日期校验:4个时间不能早于启动时间;启动时间必填 - 详情页新增耗时统计区块(总/产品/研发/测试耗时) - 优先级和状态合并同一行 - 新增'未开始'状态 - 表格垂直居中对齐 - renderProducts 后重新初始化 lucide 图标
This commit is contained in:
@@ -23,7 +23,7 @@ function openDrawer(resource, id) {
|
||||
? [["project_name","项目名称"],["owner","负责人"],["expected_sign_date","截止时间"],["expected_contract_amount","金额"],["notes","项目说明"]]
|
||||
: resource === "proposals"
|
||||
? [["customer_or_project_name","客户/项目"],["proposal_type","方案类型"],["notes","备注"]]
|
||||
: [["product_name","产品名称"],["version","版本号"],["version_goal","版本目标"],["feature_list","核心功能"],["launch_date","上线日期"],["status","状态"],["notes","备注"]];
|
||||
: [["product_name","版本名称"],["version","版本号"],["priority","优先级"],["version_goal","版本目标"],["start_date","启动时间"],["plan_date","产品方案"],["dev_done_date","研发完成"],["test_date","测试完成"],["launch_date","上线时间"],["notes","进展备注"]];
|
||||
const fieldIcons = {
|
||||
target_customer: "user", priority: "flag", status: "circle-dot",
|
||||
project_name: "briefcase-business", project_version: "git-branch", project_status: "circle-dot", current_stage: "map-pin",
|
||||
@@ -31,29 +31,38 @@ function openDrawer(resource, id) {
|
||||
sign_probability: "percent", sop_stage: "list-checks", execution_progress: "activity",
|
||||
current_deliverable: "package", risks: "alert-triangle", next_action: "arrow-right",
|
||||
product_name: "box", version: "tag", version_goal: "target", feature_list: "list", platform: "layers",
|
||||
launch_date: "calendar", notes: "sticky-note", proposal_type: "tag", customer_or_project_name: "building"
|
||||
launch_date: "calendar", notes: "sticky-note", proposal_type: "tag", customer_or_project_name: "building",
|
||||
priority: "flag", owner: "user", start_date: "play", plan_date: "file-text", dev_done_date: "check-square",
|
||||
test_date: "bug", devs: "users", testers: "shield-check"
|
||||
};
|
||||
const multilineFields = ["customer_need", "current_deliverable", "risks", "next_action", "version_goal", "feature_list", "notes"];
|
||||
const followupTarget = resource === "sales" ? "sales" : resource === "proposals" ? "proposal" : resource === "operations" ? "operation" : resource === "products" ? "product" : "";
|
||||
const title = esc(item.target_customer || item.project_name || (item.customer_or_project_name ? `${item.customer_or_project_name} · ${item.proposal_type || ''}` : "") || item.product_name);
|
||||
const titleForAttr = esc(item.target_customer || item.project_name || (item.customer_or_project_name ? `${item.customer_or_project_name} · ${item.proposal_type || ''}` : "") || item.product_name);
|
||||
drawer.innerHTML = `<div class="drawer-panel"><div class="sticky top-0 z-10 flex items-center justify-between border-b border-slate-200 bg-white/95 px-5 py-3 backdrop-blur"><div><p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-400">Detail Drawer</p><div class="flex items-center gap-2"><h2 class="drawer-title text-[17px] font-semibold leading-6 text-slate-900">${title}</h2><span id="drawerSaveStatus" class="save-status"></span></div></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hover:bg-red-50" onclick="deleteDrawerItem('${resource}', ${id})"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm" onclick="closeDrawer()">关闭</button></div></div><div class="grid gap-5 p-5">
|
||||
${resource === "products" ? (() => {
|
||||
const dDays = (s, e) => { if (!s || !e) return '-'; const d = Math.round((new Date(e) - new Date(s)) / 86400000); return d >= 0 ? d + ' 天' : '-'; };
|
||||
return `<section>
|
||||
<h3 class="drawer-section-title">耗时统计</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="bg-slate-50 rounded-lg p-3"><p class="text-xs text-slate-500">总耗时(上线−启动)</p><p class="text-lg font-semibold text-slate-800 mt-1">${dDays(item.start_date, item.launch_date)}</p></div>
|
||||
<div class="bg-slate-50 rounded-lg p-3"><p class="text-xs text-slate-500">产品耗时(方案−启动)</p><p class="text-lg font-semibold text-slate-800 mt-1">${dDays(item.start_date, item.plan_date)}</p></div>
|
||||
<div class="bg-slate-50 rounded-lg p-3"><p class="text-xs text-slate-500">研发耗时(研发完成−方案)</p><p class="text-lg font-semibold text-slate-800 mt-1">${dDays(item.plan_date, item.dev_done_date)}</p></div>
|
||||
<div class="bg-slate-50 rounded-lg p-3"><p class="text-xs text-slate-500">测试耗时(测试完成−研发完成)</p><p class="text-lg font-semibold text-slate-800 mt-1">${dDays(item.dev_done_date, item.test_date)}</p></div>
|
||||
</div>
|
||||
</section>`;
|
||||
})() : ""}
|
||||
<section>
|
||||
<h3 class="drawer-section-title">属性</h3>
|
||||
<form id="drawerForm" class="drawer-fields">
|
||||
${resource === "operations" ? drawerField("map-pin", "当前阶段", "current_stage", "", false, `<select name="current_stage" class="form-ctrl" onchange="saveDrawerField(this,'${resource}',${id})">${["商务洽谈","系统上线","团队分工","项目交付","上线推广","结项验收"].map((s) => `<option ${s === item.current_stage ? "selected" : ""}>${s}</option>`).join("")}</select>`) : ""}
|
||||
${fields.map(([key,label]) => {
|
||||
if (resource === "products" && key === "feature_list") {
|
||||
const features = (item[key] || "").split("\n").filter(Boolean);
|
||||
if (features.length === 0) features.push("");
|
||||
return `<div class="drawer-field"><div class="drawer-field-label"><i data-lucide="list"></i><span>${label}</span></div><div class="drawer-field-control" data-field="feature_list" data-id="${id}"><div class="feature-list" id="featureList_${id}">${features.map((f,i) => `<div class="feature-item"><span class="feature-num">${i+1}.</span><input class="form-ctrl" value="${f.replace(/"/g,'"')}" onchange="saveFeatureList(${id})"><button class="feature-del" onclick="event.preventDefault();removeFeature(${id},${i})"><i data-lucide="x" style="width:12px;height:12px"></i></button></div>`).join("")}</div><button class="btn btn-ghost btn-sm text-blue-600 mt-1" onclick="event.preventDefault();addFeature(${id})"><i data-lucide="plus" style="width:14px;height:14px"></i>添加功能</button></div></div>`;
|
||||
if (resource === "products" && key === "priority") {
|
||||
return `<div class="drawer-field"><div class="grid grid-cols-2 gap-3"><div><div class="drawer-field-label"><i data-lucide="flag"></i><span>优先级</span></div><select name="priority" class="form-ctrl" onchange="saveDrawerField(this,'${resource}',${id})">${["P0","P1","P2","P3"].map((s) => `<option ${s === (item.priority||'P2') ? "selected" : ""}>${s}</option>`).join("")}</select></div><div><div class="drawer-field-label"><i data-lucide="circle-dot"></i><span>状态</span></div><select name="status" class="form-ctrl" onchange="saveDrawerField(this,'${resource}',${id})">${["未开始","规划中","开发中","测试中","已上线","已取消"].map((s) => `<option ${s === (item.status||'规划中') ? "selected" : ""}>${s}</option>`).join("")}</select></div></div></div>`;
|
||||
}
|
||||
if (resource === "products" && key === "launch_date") {
|
||||
if (resource === "products" && (key === "start_date" || key === "plan_date" || key === "dev_done_date" || key === "test_date" || key === "launch_date")) {
|
||||
return drawerField("calendar", label, key, item[key], false, `<input type="date" name="${key}" value="${item[key]||''}" class="form-ctrl" data-original="${item[key]||''}" onchange="saveDrawerField(this,'${resource}',${id})">`);
|
||||
}
|
||||
if (resource === "products" && key === "status") {
|
||||
return drawerField("circle-dot", label, key, "", false, `<select name="status" class="form-ctrl" onchange="saveDrawerField(this,'${resource}',${id})">${["规划中","开发中","测试中","已上线","已取消"].map((s) => `<option ${s === (item.status||'规划中') ? "selected" : ""}>${s}</option>`).join("")}</select>`);
|
||||
}
|
||||
return drawerField(fieldIcons[key] || "circle", label, key, item[key], multilineFields.includes(key));
|
||||
}).join("")}
|
||||
</form>
|
||||
@@ -228,6 +237,27 @@ window.deleteFollowup = async (event, followupId, resource, targetId) => {
|
||||
window.saveDrawerField = async (el, resource, id) => {
|
||||
const name = el.name;
|
||||
const value = el.value;
|
||||
// 产品日期约束
|
||||
if (resource === "products") {
|
||||
const listKey = "products";
|
||||
const product = (state.data[listKey] || []).find(x => x.id === id);
|
||||
if (product) {
|
||||
// 启动时间必填
|
||||
if (name === "start_date" && !value) {
|
||||
toast("启动时间为必填项", "error");
|
||||
el.value = product.start_date || '';
|
||||
el.focus();
|
||||
return;
|
||||
}
|
||||
// 其他 4 个时间不能早于启动时间
|
||||
if (["plan_date","dev_done_date","test_date","launch_date"].includes(name) && value && product.start_date && value < product.start_date) {
|
||||
toast("该时间不能早于启动时间(" + product.start_date + ")", "error");
|
||||
el.value = product[name] || '';
|
||||
el.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
await api(`/api/${resource}/${id}`, { method: "PUT", body: JSON.stringify({ data: { [name]: value } }) });
|
||||
const listKey = { sales: "sales", proposals: "proposals", operations: "operations", products: "products" }[resource];
|
||||
|
||||
Reference in New Issue
Block a user