Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c26cbb3cf4 | ||
|
|
296917456c | ||
|
|
236d43052e |
@@ -666,6 +666,26 @@ def bootstrap():
|
||||
|
||||
# ---------- 通用 CRUD ----------
|
||||
|
||||
@bp.route("/api/<resource>/list", methods=["GET"])
|
||||
@login_required
|
||||
def list_resource(resource):
|
||||
"""轻量级列表接口,只返回指定 tenant 的记录"""
|
||||
if resource not in TABLES:
|
||||
return jsonify({"error": "unknown resource"}), 404
|
||||
table, _ = TABLES[resource]
|
||||
tenant = request.args.get("tenant", session.get("tenants", [""])[0])
|
||||
allowed = session.get("tenants", [])
|
||||
allowed = [t if t != "总工作台" else "总览" for t in allowed]
|
||||
allowed = [t if t != "学会·无界" else "学术·无界" for t in allowed]
|
||||
if tenant not in allowed:
|
||||
tenant = allowed[0] if allowed else ""
|
||||
conn = db()
|
||||
try:
|
||||
result = rows(conn, f"SELECT * FROM {table} WHERE tenant=? ORDER BY id DESC", [tenant])
|
||||
return jsonify(result)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@bp.route("/api/<resource>", methods=["POST"])
|
||||
@login_required
|
||||
def create_resource(resource):
|
||||
|
||||
@@ -175,7 +175,9 @@ window.deleteDrawerItem = async (resource, id) => {
|
||||
logActivity(resType, id, "删除了「" + name + "」");
|
||||
}
|
||||
closeDrawer();
|
||||
await load();
|
||||
var rk = resource === 'projectFinances' ? 'projectFinances' : resource === 'planFinances' ? 'planFinances' : null;
|
||||
if (rk) { try { state.data[rk] = await api("/api/" + rk + "/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {} }
|
||||
if (resource === 'projectFinances') renderFinance(); else if (resource === 'planFinances') renderPlan();
|
||||
} catch (error) {
|
||||
toast("删除失败:" + error.message, "error");
|
||||
}
|
||||
@@ -216,21 +218,26 @@ window.submitComment = async (event, targetType, targetId, resource) => {
|
||||
button.disabled = true;
|
||||
button.textContent = "发送中…";
|
||||
await api(`/api/followups/${targetType}/${targetId}`, { method: "POST", body: JSON.stringify({ data: { content } }) });
|
||||
await load();
|
||||
var ck = resource === 'projectFinances' ? 'projectFinances' : resource === 'planFinances' ? 'planFinances' : null;
|
||||
if (ck) { try { state.data[ck] = await api("/api/" + ck + "/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {} }
|
||||
openDrawer(resource, targetId);
|
||||
};
|
||||
|
||||
window.deleteActivity = async (id) => {
|
||||
if (!confirm("确认删除这条动态?")) return;
|
||||
await api(`/api/followups/${id}`, { method: "DELETE" });
|
||||
await load();
|
||||
var dk = resource === 'projectFinances' ? 'projectFinances' : resource === 'planFinances' ? 'planFinances' : null;
|
||||
if (dk) { try { state.data[dk] = await api("/api/" + dk + "/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {} }
|
||||
if (resource === 'projectFinances') renderFinance(); else if (resource === 'planFinances') renderPlan();
|
||||
};
|
||||
|
||||
window.deleteFollowup = async (event, followupId, resource, targetId) => {
|
||||
event.stopPropagation();
|
||||
if (!confirm("确认删除这条评论?")) return;
|
||||
await api(`/api/followups/${followupId}`, { method: "DELETE" });
|
||||
await load();
|
||||
var fkey2 = resource === 'projectFinances' ? 'projectFinances' : resource === 'planFinances' ? 'planFinances' : null;
|
||||
if (fkey2) { try { state.data[fkey2] = await api("/api/" + fkey2 + "/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {} }
|
||||
if (resource === 'projectFinances') renderFinance(); else if (resource === 'planFinances') renderPlan();
|
||||
openDrawer(resource, targetId);
|
||||
};
|
||||
|
||||
|
||||
@@ -113,14 +113,15 @@ function renderFinance() {
|
||||
const finHeaderBase = `<div class="flex items-center gap-2"><button onclick="setFinView('overview')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='overview'?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">总视图</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">按季度:</span>${qLabels.map((q,i) => `<button onclick="setFinQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='quarterly'&&activeQ===i?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">按月度:</span>${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return `<button onclick="setFinMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.finView==='monthly'&&state.finMonth===ms?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
|
||||
const finAddBtn = `<button class="btn btn-primary btn-sm" onclick="openFinanceModal()">新增财务项目</button>`;
|
||||
|
||||
const finFilterTabs = `<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0"><button onclick="switchFinFilter('overview')" class="finance-tab${state.finFilter==='overview'?' active':''}" style="font-size:14px;font-weight:600">经营月报</button><button onclick="switchFinFilter('quarterlyOverview')" class="finance-tab${state.finFilter==='quarterlyOverview'?' active':''}" style="font-size:14px;font-weight:600">季度月报</button><button onclick="switchFinFilter('projects')" class="finance-tab${state.finFilter==='projects'?' active':''}" style="font-size:14px;font-weight:600">项目 (${pfs.length})</button><button onclick="switchFinFilter('expense')" class="finance-tab${state.finFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用</button></div>`;
|
||||
const isOverviewMode = state.tenant === "总览";
|
||||
const finFilterTabs = `<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0"><button onclick="switchFinFilter('overview')" class="finance-tab${state.finFilter==='overview'?' active':''}" style="font-size:14px;font-weight:600">经营月报</button><button onclick="switchFinFilter('quarterlyOverview')" class="finance-tab${state.finFilter==='quarterlyOverview'?' active':''}" style="font-size:14px;font-weight:600">经营季报</button>${isOverviewMode ? '' : `<button onclick="switchFinFilter('projects')" class="finance-tab${state.finFilter==='projects'?' active':''}" style="font-size:14px;font-weight:600">项目 (${pfs.length})</button><button onclick="switchFinFilter('expense')" class="finance-tab${state.finFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用</button>`}</div>`;
|
||||
|
||||
document.querySelector("#finance").innerHTML = `<div class="grid gap-2">
|
||||
${finFilterTabs}
|
||||
<div class="flex items-center gap-3 px-4 py-2 rounded-lg border" style="background:linear-gradient(135deg,#dbeafe,#bfdbfe);border-color:#3b82f6">
|
||||
${isOverviewMode ? '' : `<div class="flex items-center gap-3 px-4 py-2 rounded-lg border" style="background:linear-gradient(135deg,#dbeafe,#bfdbfe);border-color:#3b82f6">
|
||||
<i data-lucide="lightbulb" class="text-blue-600 flex-shrink-0" style="width:18px;height:18px"></i>
|
||||
<span class="text-sm text-blue-900"><strong>执行提示:</strong>先关注确收,毛利确保业务运营,再关注回款,回款率,最后逐步优化成本与支出</span>
|
||||
</div>
|
||||
</div>`}
|
||||
${state.finFilter !== 'expense' && state.finFilter !== 'overview' && state.finFilter !== 'quarterlyOverview' ? card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "py-2 px-4") : ''}
|
||||
<div id="financeModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closeFinanceModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="financeModalTitle">新增项目财务</h3><p class="text-xs text-slate-400 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="financeDeleteBtn" onclick="deleteFinanceItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeFinanceModal()"><i data-lucide="x"></i></button></div></div>
|
||||
<div class="finance-tabs">
|
||||
@@ -907,7 +908,8 @@ window.createFinance = async (event) => {
|
||||
document.querySelector("#pf-id-input").value = "";
|
||||
document.querySelector("#financeModalTitle").textContent = "新增项目财务";
|
||||
closeFinanceModal();
|
||||
await load();
|
||||
state.data.projectFinances = await api("/api/projectFinances/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
renderFinance();
|
||||
} catch (error) {
|
||||
toast("保存失败:" + error.message, "error");
|
||||
}
|
||||
@@ -922,7 +924,8 @@ window.deleteFinanceItem = async () => {
|
||||
try {
|
||||
await api(`/api/projectFinances/${pfId}`, { method: "DELETE" });
|
||||
closeFinanceModal();
|
||||
await load();
|
||||
state.data.projectFinances = await api("/api/projectFinances/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
renderFinance();
|
||||
toast("已删除", "success");
|
||||
} catch (error) {
|
||||
toast("删除失败:" + error.message, "error");
|
||||
@@ -1047,7 +1050,7 @@ function renderFinanceOverview() {
|
||||
}
|
||||
}
|
||||
|
||||
// 季度月报渲染
|
||||
// 经营季报渲染
|
||||
function renderFinanceQuarterlyOverview() {
|
||||
const { summary, financeMonthly } = state.data;
|
||||
if (!summary) return;
|
||||
|
||||
@@ -111,14 +111,15 @@ function renderPlan() {
|
||||
const finHeaderBase = `<div class="flex items-center gap-2"><button onclick="setPlanView('overview')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='overview'?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">总视图</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">按季度:</span>${qLabels.map((q,i) => `<button onclick="setPlanQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='quarterly'&&activeQ===i?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">按月度:</span>${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return `<button onclick="setPlanMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='monthly'&&state.planMonth===ms?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
|
||||
const finAddBtn = `<button class="btn btn-primary btn-sm" onclick="openPlanModal()">新增财务项目</button>`;
|
||||
|
||||
const planFilterTabs = `<div class="finance-tabs" style="padding:0 0 0 0;border-bottom:1px solid #e2e8f0;margin-bottom:0;display:flex;gap:4px"><button onclick="switchPlanFilter('overview')" class="finance-tab${state.planFilter==='overview'?' active':''}" style="font-size:14px;font-weight:600;padding:8px 16px;margin-right:0">经营月报</button><button onclick="switchPlanFilter('quarterlyOverview')" class="finance-tab${state.planFilter==='quarterlyOverview'?' active':''}" style="font-size:14px;font-weight:600">季度月报</button><button onclick="switchPlanFilter('projects')" class="finance-tab${state.planFilter==='projects'?' active':''}" style="font-size:14px;font-weight:600">项目计划 (${pfs.length})</button><button onclick="switchPlanFilter('expense')" class="finance-tab${state.planFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用计划</button></div>`;
|
||||
const isOverviewMode = state.tenant === "总览";
|
||||
const planFilterTabs = `<div class="finance-tabs" style="padding:0 0 0 0;border-bottom:1px solid #e2e8f0;margin-bottom:0;display:flex;gap:4px"><button onclick="switchPlanFilter('overview')" class="finance-tab${state.planFilter==='overview'?' active':''}" style="font-size:14px;font-weight:600;padding:8px 16px;margin-right:0">经营月报</button><button onclick="switchPlanFilter('quarterlyOverview')" class="finance-tab${state.planFilter==='quarterlyOverview'?' active':''}" style="font-size:14px;font-weight:600">经营季报</button>${isOverviewMode ? '' : `<button onclick="switchPlanFilter('projects')" class="finance-tab${state.planFilter==='projects'?' active':''}" style="font-size:14px;font-weight:600">项目计划 (${pfs.length})</button><button onclick="switchPlanFilter('expense')" class="finance-tab${state.planFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用计划</button>`}</div>`;
|
||||
|
||||
document.querySelector("#plan").innerHTML = `<div class="grid gap-2">
|
||||
${planFilterTabs}
|
||||
<div class="flex items-center gap-3 px-4 py-2 rounded-lg border" style="background:linear-gradient(135deg,#dbeafe,#bfdbfe);border-color:#3b82f6">
|
||||
${isOverviewMode ? '' : `<div class="flex items-center gap-3 px-4 py-2 rounded-lg border" style="background:linear-gradient(135deg,#dbeafe,#bfdbfe);border-color:#3b82f6">
|
||||
<i data-lucide="lightbulb" class="text-blue-600 flex-shrink-0" style="width:18px;height:18px"></i>
|
||||
<span class="text-sm text-blue-900"><strong>计划提醒:</strong>计划也是预算,只有真正能够列出计划的预算,才有靠谱的落地可能性</span>
|
||||
</div>
|
||||
</div>`}
|
||||
${state.planFilter !== 'expense' && state.planFilter !== 'overview' && state.planFilter !== 'quarterlyOverview' ? card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "py-2 px-4") : ''}
|
||||
<div id="planModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closePlanModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="planModalTitle">新增项目财务</h3><p class="text-xs text-slate-400 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="planDeleteBtn" onclick="deletePlanItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closePlanModal()"><i data-lucide="x"></i></button></div></div>
|
||||
<div class="finance-tabs">
|
||||
@@ -944,7 +945,8 @@ window.createPlanFinance = async (event) => {
|
||||
document.querySelector("#plan_pf-id-input").value = "";
|
||||
document.querySelector("#planModalTitle").textContent = "新增项目财务";
|
||||
closePlanModal();
|
||||
await load();
|
||||
state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
renderPlan();
|
||||
} catch (error) {
|
||||
toast("刷新失败:" + error.message, "error");
|
||||
}
|
||||
@@ -959,7 +961,8 @@ window.deletePlanItem = async () => {
|
||||
try {
|
||||
await api(`/api/planFinances/${pfId}`, { method: "DELETE" });
|
||||
closePlanModal();
|
||||
await load();
|
||||
state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
renderPlan();
|
||||
toast("已删除", "success");
|
||||
} catch (error) {
|
||||
toast("删除失败:" + error.message, "error");
|
||||
@@ -1284,7 +1287,8 @@ function planInlineSelect(name, pfId, field, currentVal, options) {
|
||||
window.planInlineUpdate = async (pfId, field, value) => {
|
||||
try {
|
||||
await api("/api/planFinances/" + pfId, { method: "PUT", body: JSON.stringify({ data: { [field]: value } }) });
|
||||
await load();
|
||||
state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
renderPlan();
|
||||
toast("已更新", "success");
|
||||
} catch (error) {
|
||||
toast("更新失败:" + error.message, "error");
|
||||
|
||||
@@ -199,8 +199,10 @@ window.savePlanExpense = async function(event) {
|
||||
return;
|
||||
}
|
||||
closePlanExpModal();
|
||||
await load();
|
||||
renderPlanExpense();
|
||||
try {
|
||||
state.data.planExpense = await api("/api/planExpense/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
renderPlanExpense();
|
||||
} catch (e) { /* non-critical */ }
|
||||
};
|
||||
|
||||
window.deletePlanExpItem = async function() {
|
||||
@@ -213,6 +215,8 @@ window.deletePlanExpItem = async function() {
|
||||
return;
|
||||
}
|
||||
closePlanExpModal();
|
||||
await load();
|
||||
renderPlanExpense();
|
||||
try {
|
||||
state.data.planExpense = await api("/api/planExpense/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
renderPlanExpense();
|
||||
} catch (e) { /* non-critical */ }
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ async function createResource(event, resource) {
|
||||
const name = data.project_name || data.target_customer || data.customer_or_project_name || data.product_name || "";
|
||||
if (result.id && name) logActivity(resType, result.id, "创建了" + name);
|
||||
form.reset();
|
||||
await load();
|
||||
try { state.data.products = await api("/api/products/list?tenant=" + encodeURIComponent(state.tenant)); renderProducts(); } catch(e) {}
|
||||
} catch (error) {
|
||||
toast("创建失败:" + error.message, "error");
|
||||
}
|
||||
@@ -141,7 +141,7 @@ window.submitProductDrawer = async (event) => {
|
||||
form.reset();
|
||||
closeProductDrawer();
|
||||
if (result.id) logActivity("product", result.id, "创建了产品版本「" + data.product_name + " " + data.version + "」");
|
||||
await load();
|
||||
try { state.data.products = await api("/api/products/list?tenant=" + encodeURIComponent(state.tenant)); renderProducts(); } catch(e) {}
|
||||
} catch (error) {
|
||||
toast("创建失败:" + error.message, "error");
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ window.duplicateProject = async () => {
|
||||
});
|
||||
}
|
||||
toast("已创建副本", "success");
|
||||
await load();
|
||||
try { state.data.operations = await api("/api/operations/list?tenant=" + encodeURIComponent(state.tenant)); renderProjects(); } catch(e) {}
|
||||
} catch (error) {
|
||||
toast("创建副本失败:" + error.message, "error");
|
||||
}
|
||||
@@ -463,7 +463,7 @@ window.submitTaskForm = async (event, projectId) => {
|
||||
const result = await api("/api/tasks", { method: "POST", body: JSON.stringify({ data }) });
|
||||
if (result.id && data.task) logActivity("task", result.id, "创建了任务「" + data.task + "」");
|
||||
closeTaskDrawer(projectId);
|
||||
await load();
|
||||
try { state.data.tasks = await api("/api/tasks/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {}
|
||||
}
|
||||
} catch (error) {
|
||||
toast("保存失败:" + error.message, "error");
|
||||
|
||||
@@ -32,7 +32,8 @@ async function ensureStandardProposals() {
|
||||
});
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
await load();
|
||||
state.data.proposals = await api("/api/proposals/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
renderProposals();
|
||||
}
|
||||
|
||||
window.switchProposalTab = (tab) => {
|
||||
@@ -183,7 +184,7 @@ window.submitStandardComment = async (event, targetId) => {
|
||||
button.disabled = true;
|
||||
button.textContent = "发送中…";
|
||||
await api(`/api/followups/proposal/${targetId}`, { method: "POST", body: JSON.stringify({ data: { content } }) });
|
||||
await load();
|
||||
try { state.data.proposals = await api("/api/proposals/list?tenant=" + encodeURIComponent(state.tenant)); renderProposals(); } catch(e) {}
|
||||
openStandardProposalDrawer(targetId);
|
||||
};
|
||||
|
||||
@@ -207,7 +208,7 @@ window.submitProposal = async (event) => {
|
||||
if (result.id && data.customer_or_project_name) logActivity("proposal", result.id, "创建了方案「" + data.customer_or_project_name + "」");
|
||||
form.reset();
|
||||
closeProposalModal();
|
||||
await load();
|
||||
try { state.data.proposals = await api("/api/proposals/list?tenant=" + encodeURIComponent(state.tenant)); renderProposals(); } catch(e) {}
|
||||
} catch (error) {
|
||||
toast("保存失败:" + error.message, "error");
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ function updateSidebarTabs() {
|
||||
document.querySelectorAll(".sidebar-tab").forEach((btn) => {
|
||||
const tab = btn.dataset.tab;
|
||||
if (isOverview) {
|
||||
btn.style.display = tab === "home" ? "" : "none";
|
||||
btn.style.display = (tab === "home" || tab === "plan" || tab === "finance") ? "" : "none";
|
||||
} else {
|
||||
btn.style.display = tab === "home" ? "none" : "";
|
||||
}
|
||||
@@ -242,7 +242,7 @@ window.switchTenant = (tenant) => {
|
||||
document.querySelector("#workspaceTitle").textContent = tenant.replace("·无界", "") + " OPC 工作台";
|
||||
if (typeof updateTenantLabel === "function") updateTenantLabel();
|
||||
updateSidebarTabs();
|
||||
if (tenant === "总览") switchTab("home");
|
||||
if (tenant === "总览") { state.planFilter = 'overview'; state.finFilter = 'overview'; switchTab("home"); }
|
||||
load();
|
||||
};
|
||||
window.doLogout = async () => {
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
||||
<div class="flex items-center gap-3 w-full">
|
||||
<div class="flex-1">
|
||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.0.0.35</span></p>
|
||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.0.0.38</span></p>
|
||||
<div class="flex items-center gap-4 mt-1">
|
||||
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1>
|
||||
<div class="hidden sm:flex items-center gap-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-full px-4 py-1.5">
|
||||
|
||||
Reference in New Issue
Block a user