Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50358e116d | ||
|
|
1112827a19 | ||
|
|
1c3b487caa |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ data/uploads/
|
|||||||
*.pyc
|
*.pyc
|
||||||
__pycache__/
|
__pycache__/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
data/opc.sqlite
|
||||||
|
|||||||
@@ -1,18 +1,41 @@
|
|||||||
# OPC Manager Version Log
|
# OPC Manager Version Log
|
||||||
|
|
||||||
## opc-manager-v0.1.0 - 2026-05-30
|
## v1.0.1 — 2026-05-30
|
||||||
|
|
||||||
- Deployed the Flask/Jinja OPC workbench to the business server.
|
- 将 data/opc.sqlite 加入 .gitignore,避免运行时数据库被误提交
|
||||||
- Runtime path: `/opt/opc-manager`.
|
|
||||||
- Runtime service: `opc-manager.service` managed by systemd.
|
|
||||||
- Runtime command: `gunicorn -w 2 -b 127.0.0.1:5177 backend.flask_app:app`.
|
|
||||||
- Public URL: `https://opc.yxcowork.vip`.
|
|
||||||
- Health check: `https://opc.yxcowork.vip/api/health`.
|
|
||||||
- Database path: `/opt/opc-manager/data/opc.sqlite`.
|
|
||||||
- Caddy route: `opc.yxcowork.vip -> localhost:5177`.
|
|
||||||
|
|
||||||
Deployment rule from this version onward:
|
## v1.0.0 — 2026-05-30
|
||||||
|
|
||||||
- Every deployment must be committed to Git.
|
**首次正式发布**
|
||||||
- Every deployment must create a corresponding Git tag.
|
|
||||||
- Every deployment must update this version log.
|
### Features
|
||||||
|
- 首页概览:7 项关键指标卡片(4 列自动换行)、财务趋势图、风险提醒、近期动态
|
||||||
|
- 销售管理:客户表格 + 抽屉详情(字段失焦自动保存)
|
||||||
|
- 业务方案:版本表格 + 抽屉(文件上传/预览/下载/删除)
|
||||||
|
- 运营管理:项目表格(业务机会/已签约执行分类筛选)+ 抽屉
|
||||||
|
- 产品研发:版本表格 + 抽屉
|
||||||
|
- 财务管理:月度收入/毛利/成本/净利曲线图 + 明细表
|
||||||
|
|
||||||
|
### Interactions
|
||||||
|
- 所有抽屉:Plane 风格紧凑布局(720px)、字段失焦自动保存、状态指示
|
||||||
|
- 评论区:Squire 富文本编辑器(加粗/斜体/下划线/删除线/无序列表/有序列表/引用/撤销/重做)
|
||||||
|
- 评论支持删除,带确认弹窗
|
||||||
|
- 评论内容保留 HTML 格式(加粗、列表等)
|
||||||
|
- 图标库:Lucide
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
- Backend: Flask + SQLite
|
||||||
|
- Frontend: Vanilla JS + Tailwind CSS CDN
|
||||||
|
- Editor: Squire (Fastmail)
|
||||||
|
- Charts: Chart.js
|
||||||
|
- Icons: Lucide
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- 首页财务图表空白问题:固定容器高度 140px + maintainAspectRatio: false
|
||||||
|
- 首页指标卡片布局:grid-cols-7 → grid-cols-4 自动换行
|
||||||
|
- 风险提醒文字竖排:grid-cols-2 等宽布局 + break-words
|
||||||
|
- 评论区工具栏按钮无效:onclick → onmousedown 防止焦点丢失
|
||||||
|
- 格式 toggle 无效:hasFormat 检测 + removeBold/removeItalic
|
||||||
|
- 列表按钮无效:Squire API 替代 Trix
|
||||||
|
- 评论内容格式丢失:encodeURIComponent 编码 + decodeURIComponent 渲染
|
||||||
|
- 列表显示无标记:list-style: revert 覆盖 Tailwind reset
|
||||||
|
|||||||
84
scripts/deploy.sh
Executable file
84
scripts/deploy.sh
Executable file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# OPC Manager 标准部署脚本
|
||||||
|
# 用法: ./scripts/deploy.sh <version> [message]
|
||||||
|
# 示例: ./scripts/deploy.sh v0.1.1 "修复首页样式"
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="${1:-}"
|
||||||
|
MESSAGE="${2:-Release $VERSION}"
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "用法: ./scripts/deploy.sh <version> [message]"
|
||||||
|
echo "示例: ./scripts/deploy.sh v0.1.1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAG="opc-manager-$VERSION"
|
||||||
|
REMOTE_HOST="${OPC_REMOTE_HOST:-business}"
|
||||||
|
REMOTE_PATH="${OPC_REMOTE_PATH:-/opt/opc-manager}"
|
||||||
|
SERVICE_NAME="${OPC_SERVICE_NAME:-opc-manager.service}"
|
||||||
|
HEALTH_URL="${OPC_HEALTH_URL:-https://opc.yxcowork.vip/api/health}"
|
||||||
|
HOME_URL="${OPC_HOME_URL:-https://opc.yxcowork.vip}"
|
||||||
|
|
||||||
|
echo "=== OPC Manager 部署 $VERSION ==="
|
||||||
|
|
||||||
|
# 1. 确保在项目根目录
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# 2. 更新 VERSION_LOG.md
|
||||||
|
echo "" >> VERSION_LOG.md
|
||||||
|
echo "## $TAG - $(date +%F)" >> VERSION_LOG.md
|
||||||
|
echo "" >> VERSION_LOG.md
|
||||||
|
echo "- $MESSAGE" >> VERSION_LOG.md
|
||||||
|
echo "" >> VERSION_LOG.md
|
||||||
|
echo "VERSION_LOG.md 已更新"
|
||||||
|
|
||||||
|
# 3. Git commit
|
||||||
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "没有需要提交的变更,跳过 commit"
|
||||||
|
else
|
||||||
|
git commit -m "release: $TAG - $MESSAGE"
|
||||||
|
echo "已提交: release: $TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Git tag(轻量 tag,只打在 OPC 独立仓库)
|
||||||
|
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||||
|
echo "Tag $TAG 已存在,跳过"
|
||||||
|
else
|
||||||
|
git tag "$TAG"
|
||||||
|
echo "已打 tag: $TAG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Git push
|
||||||
|
echo "推送代码和 tag..."
|
||||||
|
git push origin main
|
||||||
|
git push origin "$TAG"
|
||||||
|
|
||||||
|
# 6. 服务器同步
|
||||||
|
echo "同步到服务器 $REMOTE_HOST..."
|
||||||
|
ssh "$REMOTE_HOST" "cd $REMOTE_PATH && git fetch origin && git reset --hard origin/main"
|
||||||
|
|
||||||
|
# 7. 重启服务
|
||||||
|
echo "重启 $SERVICE_NAME..."
|
||||||
|
ssh "$REMOTE_HOST" "sudo systemctl restart $SERVICE_NAME && sleep 2"
|
||||||
|
ssh "$REMOTE_HOST" "systemctl status $SERVICE_NAME --no-pager" | head -6
|
||||||
|
|
||||||
|
# 8. 健康检查
|
||||||
|
echo ""
|
||||||
|
echo "健康检查..."
|
||||||
|
sleep 2
|
||||||
|
HEALTH_RESULT=$(curl -s "$HEALTH_URL")
|
||||||
|
echo "$HEALTH_RESULT"
|
||||||
|
|
||||||
|
HOME_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$HOME_URL")
|
||||||
|
echo "首页 HTTP 状态: $HOME_CODE"
|
||||||
|
|
||||||
|
if [ "$HOME_CODE" = "200" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "=== 部署 $VERSION 完成 ==="
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "!!! 警告:首页返回 $HOME_CODE,请检查 !!!"
|
||||||
|
fi
|
||||||
112
static/app.js
112
static/app.js
@@ -65,6 +65,28 @@ function render() {
|
|||||||
renderProducts();
|
renderProducts();
|
||||||
renderFinance();
|
renderFinance();
|
||||||
if (window.lucide) window.lucide.createIcons();
|
if (window.lucide) window.lucide.createIcons();
|
||||||
|
// Decode and render rich HTML content in followup records
|
||||||
|
drawer.querySelectorAll(".rich-content").forEach((el) => {
|
||||||
|
const html = el.dataset.html;
|
||||||
|
if (html) el.innerHTML = decodeURIComponent(html);
|
||||||
|
});
|
||||||
|
// Initialize Squire editor
|
||||||
|
const squireDiv = drawer.querySelector(".squire-editor");
|
||||||
|
if (squireDiv && window.Squire) {
|
||||||
|
const id = squireDiv.id;
|
||||||
|
if (window.squireInstances[id]) window.squireInstances[id].destroy();
|
||||||
|
const sq = new Squire(squireDiv, { blockTag: "P" });
|
||||||
|
sq.addEventListener("input", () => {
|
||||||
|
const form = squireDiv.closest("form");
|
||||||
|
const btn = form.querySelector(".comment-submit");
|
||||||
|
});
|
||||||
|
window.squireInstances[id] = sq;
|
||||||
|
// Handle placeholder
|
||||||
|
squireDiv.addEventListener("focus", () => squireDiv.classList.add("focused"));
|
||||||
|
squireDiv.addEventListener("blur", () => {
|
||||||
|
if (!squireDiv.textContent.trim()) squireDiv.classList.remove("focused");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHome() {
|
function renderHome() {
|
||||||
@@ -72,7 +94,7 @@ function renderHome() {
|
|||||||
const m = summary.metrics;
|
const m = summary.metrics;
|
||||||
document.querySelector("#home").innerHTML = `
|
document.querySelector("#home").innerHTML = `
|
||||||
<div class="grid gap-5">
|
<div class="grid gap-5">
|
||||||
<div class="grid grid-cols-7 gap-3">
|
<div class="grid grid-cols-4 gap-3">
|
||||||
${[
|
${[
|
||||||
["P0 客户数", m.p0_customers, "sales"],
|
["P0 客户数", m.p0_customers, "sales"],
|
||||||
["跟进中销售机会", m.active_sales, "sales"],
|
["跟进中销售机会", m.active_sales, "sales"],
|
||||||
@@ -83,11 +105,11 @@ function renderHome() {
|
|||||||
["即将上线版本", m.upcoming_products, "products"],
|
["即将上线版本", m.upcoming_products, "products"],
|
||||||
].map(([label, value, tab]) => `<button class="metric-card" onclick="switchTab('${tab}')"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="gauge"></i>${label}</span><strong class="mt-2 block text-2xl">${value}</strong></button>`).join("")}
|
].map(([label, value, tab]) => `<button class="metric-card" onclick="switchTab('${tab}')"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="gauge"></i>${label}</span><strong class="mt-2 block text-2xl">${value}</strong></button>`).join("")}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-[1.35fr_0.65fr] gap-5">
|
<div class="grid grid-cols-2 gap-5">
|
||||||
${card(`<div class="mb-4 flex items-center justify-between"><h2 class="text-lg font-bold">财务趋势</h2>${badge("YYYY-MM")}</div><canvas id="financeChart" height="125"></canvas>`, "p-5")}
|
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">财务趋势</h2>${badge("YYYY-MM")}</div><div style="position:relative;height:140px"><canvas id="financeChart"></canvas></div>`, "p-4")}
|
||||||
${card(`<h2 class="text-lg font-bold">风险提醒</h2><div class="mt-3 grid gap-2">${(summary.risks.length ? summary.risks : [{ title: "暂无高风险", content: "当前无明确阻塞,按周更新即可。" }]).map((r) => `<div class="rounded-md border border-amber-200 bg-amber-50 p-3"><p class="font-bold text-amber-900">${r.title}</p><p class="mt-1 text-sm text-amber-800">${r.content}</p></div>`).join("")}</div>`, "p-5")}
|
${card(`<h2 class="text-lg font-bold">风险提醒</h2><div class="mt-3 grid gap-2">${(summary.risks.length ? summary.risks : [{ title: "暂无高风险", content: "当前无明确阻塞,按周更新即可。" }]).map((r) => `<div class="rounded-md border border-amber-200 bg-amber-50 p-3"><p class="font-bold text-amber-900">${r.title}</p><p class="mt-1 text-sm text-amber-800 break-words">${r.content}</p></div>`).join("")}</div>`, "p-5")}
|
||||||
</div>
|
</div>
|
||||||
${card(`<h2 class="text-lg font-bold">近期动态</h2><div class="mt-4 grid gap-2">${summary.recent.map((r) => `<div class="flex justify-between rounded-md bg-slate-50 px-3 py-2 text-sm"><span>${r.content}</span><span class="text-slate-500">${r.followed_at}</span></div>`).join("")}</div>`, "p-5")}
|
${card(`<h2 class="text-lg font-bold">近期动态</h2><div class="mt-4 grid gap-2">${summary.recent.map((r) => `<div class="flex justify-between rounded-md bg-slate-50 px-3 py-2 text-sm"><span class="break-words">${r.content}</span><span class="text-slate-500">${r.followed_at}</span></div>`).join("")}</div>`, "p-5")}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
renderChart(financeMonthly);
|
renderChart(financeMonthly);
|
||||||
@@ -108,7 +130,7 @@ function renderChart(data) {
|
|||||||
{ label: "净利", data: data.map((x) => x.net_profit), borderColor: "#7c3aed", tension: 0.3 },
|
{ label: "净利", data: data.map((x) => x.net_profit), borderColor: "#7c3aed", tension: 0.3 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
options: { responsive: true, plugins: { legend: { position: "bottom" } } },
|
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "bottom", labels: { boxWidth: 12, font: { size: 11 } } } }, scales: { x: { ticks: { font: { size: 11 } }, grid: { display: false } }, y: { ticks: { font: { size: 11 } } } } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +272,7 @@ function renderChartOn(id, data) {
|
|||||||
{ label: "净利", data: data.map((x) => x.net_profit), borderColor: "#7c3aed", tension: 0.3 },
|
{ label: "净利", data: data.map((x) => x.net_profit), borderColor: "#7c3aed", tension: 0.3 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
options: { responsive: true, plugins: { legend: { position: "bottom" } } },
|
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "bottom", labels: { boxWidth: 12, font: { size: 11 } } } }, scales: { x: { ticks: { font: { size: 11 } }, grid: { display: false } }, y: { ticks: { font: { size: 11 } } } } },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,12 +318,24 @@ function openDrawer(resource, id) {
|
|||||||
${resource === "proposals" ? `<section><h3 class="drawer-section-title">方案文件</h3><div class="grid gap-2">${["方案","成本","SOP","财务流程"].map((cat) => fileGroup("proposal", item.id, item.version, cat, item.files.filter((f) => f.file_category === cat))).join("")}</div></section>` : ""}
|
${resource === "proposals" ? `<section><h3 class="drawer-section-title">方案文件</h3><div class="grid gap-2">${["方案","成本","SOP","财务流程"].map((cat) => fileGroup("proposal", item.id, item.version, cat, item.files.filter((f) => f.file_category === cat))).join("")}</div></section>` : ""}
|
||||||
${followupTarget ? `<section>
|
${followupTarget ? `<section>
|
||||||
<h3 class="drawer-section-title">活动 / 跟进</h3>
|
<h3 class="drawer-section-title">活动 / 跟进</h3>
|
||||||
<div class="grid gap-2">${(item.followups || []).map((f) => `<div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span>${f.follower} · ${f.follow_up_method}</span><span>${f.followed_at}</span></div><div class="mt-1 leading-5 text-slate-800 trix-content">${f.content}</div>${f.next_action ? `<p class="mt-1 leading-5 text-blue-700">下一步:${text(f.next_action)}</p>` : ""}</div><button class="activity-delete" onclick="deleteFollowup(event, ${f.id}, '${resource}', ${item.id})" title="删除评论"><i data-lucide="trash-2"></i></button></div>`).join("")}</div>
|
<div class="grid gap-2">${(item.followups || []).map((f) => `<div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span>${f.follower} · ${f.follow_up_method}</span><span>${f.followed_at}</span></div><div class="mt-1 leading-5 text-slate-800 rich-content" data-html="${encodeURIComponent(f.content || '')}"></div>${f.next_action ? `<p class="mt-1 leading-5 text-blue-700">下一步:${text(f.next_action)}</p>` : ""}</div><button class="activity-delete" onclick="deleteFollowup(event, ${f.id}, '${resource}', ${item.id})" title="删除评论"><i data-lucide="trash-2"></i></button></div>`).join("")}</div>
|
||||||
<form class="comment-box mt-3" onsubmit="submitComment(event,'${followupTarget}',${item.id},'${resource}')">
|
<form class="comment-box mt-3" onsubmit="submitComment(event,'${followupTarget}',${item.id},'${resource}')">
|
||||||
<input id="commentHidden_${resource}_${item.id}" type="hidden" name="content">
|
<div class="squire-toolbar">
|
||||||
<trix-editor input="commentHidden_${resource}_${item.id}" placeholder="添加评论" class="comment-trix"></trix-editor>
|
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('bold')" title="加粗"><i data-lucide="bold"></i></button>
|
||||||
|
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('italic')" title="斜体"><i data-lucide="italic"></i></button>
|
||||||
|
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('underline')" title="下划线"><i data-lucide="underline"></i></button>
|
||||||
|
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('strikethrough')" title="删除线"><i data-lucide="strikethrough"></i></button>
|
||||||
|
<span class="squire-sep"></span>
|
||||||
|
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('makeUnorderedList')" title="无序列表"><i data-lucide="list"></i></button>
|
||||||
|
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('makeOrderedList')" title="有序列表"><i data-lucide="list-ordered"></i></button>
|
||||||
|
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('blockquote')" title="引用"><i data-lucide="quote"></i></button>
|
||||||
|
<span class="squire-sep"></span>
|
||||||
|
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('undo')" title="撤销"><i data-lucide="undo"></i></button>
|
||||||
|
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('redo')" title="重做"><i data-lucide="redo"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="squire-editor" id="squire_${resource}_${item.id}" placeholder="添加评论"></div>
|
||||||
<div class="comment-toolbar">
|
<div class="comment-toolbar">
|
||||||
<span class="comment-hint">支持 Markdown 格式</span>
|
<span class="comment-hint">支持富文本编辑</span>
|
||||||
<button class="btn btn-primary btn-sm comment-submit" type="submit">评论</button>
|
<button class="btn btn-primary btn-sm comment-submit" type="submit">评论</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -310,6 +344,28 @@ function openDrawer(resource, id) {
|
|||||||
drawer.classList.add("open");
|
drawer.classList.add("open");
|
||||||
bindDrawerAutosave(resource, item.id, item);
|
bindDrawerAutosave(resource, item.id, item);
|
||||||
if (window.lucide) window.lucide.createIcons();
|
if (window.lucide) window.lucide.createIcons();
|
||||||
|
// Decode and render rich HTML content in followup records
|
||||||
|
drawer.querySelectorAll(".rich-content").forEach((el) => {
|
||||||
|
const html = el.dataset.html;
|
||||||
|
if (html) el.innerHTML = decodeURIComponent(html);
|
||||||
|
});
|
||||||
|
// Initialize Squire editor
|
||||||
|
const squireDiv = drawer.querySelector(".squire-editor");
|
||||||
|
if (squireDiv && window.Squire) {
|
||||||
|
const id = squireDiv.id;
|
||||||
|
if (window.squireInstances[id]) window.squireInstances[id].destroy();
|
||||||
|
const sq = new Squire(squireDiv, { blockTag: "P" });
|
||||||
|
sq.addEventListener("input", () => {
|
||||||
|
const form = squireDiv.closest("form");
|
||||||
|
const btn = form.querySelector(".comment-submit");
|
||||||
|
});
|
||||||
|
window.squireInstances[id] = sq;
|
||||||
|
// Handle placeholder
|
||||||
|
squireDiv.addEventListener("focus", () => squireDiv.classList.add("focused"));
|
||||||
|
squireDiv.addEventListener("blur", () => {
|
||||||
|
if (!squireDiv.textContent.trim()) squireDiv.classList.remove("focused");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDrawerSaveStatus(message, tone = "muted") {
|
function setDrawerSaveStatus(message, tone = "muted") {
|
||||||
@@ -352,12 +408,36 @@ function bindDrawerAutosave(resource, id, item) {
|
|||||||
|
|
||||||
window.openDrawer = openDrawer;
|
window.openDrawer = openDrawer;
|
||||||
window.closeDrawer = () => document.querySelector("#drawer").classList.remove("open");
|
window.closeDrawer = () => document.querySelector("#drawer").classList.remove("open");
|
||||||
|
window.squireInstances = {};
|
||||||
|
window.squireCmd = (cmd) => {
|
||||||
|
const currentEditor = document.querySelector(".squire-editor");
|
||||||
|
if (!currentEditor) return;
|
||||||
|
const id = currentEditor.id;
|
||||||
|
const sq = window.squireInstances[id];
|
||||||
|
if (!sq) return;
|
||||||
|
sq.focus();
|
||||||
|
setTimeout(() => {
|
||||||
|
if (cmd === "bold") {
|
||||||
|
sq.hasFormat("b") || sq.hasFormat("strong") ? sq.removeBold() : sq.bold();
|
||||||
|
} else if (cmd === "italic") {
|
||||||
|
sq.hasFormat("i") || sq.hasFormat("em") ? sq.removeItalic() : sq.italic();
|
||||||
|
} else if (cmd === "underline") {
|
||||||
|
sq.hasFormat("u") ? sq.changeFormat(null, { tag: "u" }, null) : sq.changeFormat({ tag: "u" }, null, null);
|
||||||
|
} else if (cmd === "strikethrough") {
|
||||||
|
sq.hasFormat("s") || sq.hasFormat("del") || sq.hasFormat("strike") ? sq.changeFormat(null, { tag: "s" }, null) : sq.changeFormat({ tag: "s" }, null, null);
|
||||||
|
} else {
|
||||||
|
sq[cmd]();
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
};
|
||||||
|
|
||||||
window.submitComment = async (event, targetType, targetId, resource) => {
|
window.submitComment = async (event, targetType, targetId, resource) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const form = event.currentTarget;
|
const form = event.currentTarget;
|
||||||
const editor = form.querySelector("trix-editor");
|
const editorDiv = form.querySelector(".squire-editor");
|
||||||
const content = editor.editor.getDocument().toString().trim();
|
const sq = window.squireInstances[editorDiv.id];
|
||||||
if (!content) return;
|
const content = sq ? sq.getHTML().trim() : "";
|
||||||
|
if (!content || content === "<div><br></div>" || content === "<p><br></p>") return;
|
||||||
const button = form.querySelector(".comment-submit");
|
const button = form.querySelector(".comment-submit");
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
button.textContent = "发送中…";
|
button.textContent = "发送中…";
|
||||||
@@ -374,10 +454,6 @@ window.deleteFollowup = async (event, followupId, resource, targetId) => {
|
|||||||
openDrawer(resource, targetId);
|
openDrawer(resource, targetId);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.querySelector("#drawer").addEventListener("click", (event) => {
|
|
||||||
if (event.target === event.currentTarget) closeDrawer();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector("#tabs").addEventListener("click", (event) => {
|
document.querySelector("#tabs").addEventListener("click", (event) => {
|
||||||
const button = event.target.closest("button[data-tab]");
|
const button = event.target.closest("button[data-tab]");
|
||||||
if (button) switchTab(button.dataset.tab);
|
if (button) switchTab(button.dataset.tab);
|
||||||
|
|||||||
@@ -157,13 +157,14 @@ td {
|
|||||||
box-shadow: -18px 0 45px rgba(15, 23, 42, 0.14);
|
box-shadow: -18px 0 45px rgba(15, 23, 42, 0.14);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: 560px;
|
width: 720px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-link {
|
.file-link {
|
||||||
color: #1d4ed8;
|
color: #1d4ed8;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-section-title {
|
.drawer-section-title {
|
||||||
@@ -313,14 +314,50 @@ td {
|
|||||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.08);
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Trix editor inside comment box */
|
/* Squire editor */
|
||||||
.comment-trix {
|
.squire-toolbar {
|
||||||
min-height: 80px;
|
align-items: center;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 5px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-trix trix-editor {
|
.squire-btn {
|
||||||
|
align-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #475569;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
height: 28px;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.15s;
|
||||||
|
width: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-btn:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-btn [data-lucide] {
|
||||||
|
height: 15px;
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-sep {
|
||||||
|
background: #e2e8f0;
|
||||||
|
display: inline-block;
|
||||||
|
height: 18px;
|
||||||
|
margin: 0 2px;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.squire-editor {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
@@ -328,30 +365,32 @@ td {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-trix trix-toolbar {
|
.squire-editor p {
|
||||||
border: 0;
|
margin: 0;
|
||||||
border-top: 1px solid #e2e8f0;
|
min-height: 1em;
|
||||||
background: #f8fafc;
|
|
||||||
padding: 6px 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-trix trix-toolbar .trix-button {
|
.squire-editor ul,
|
||||||
border-radius: 4px;
|
.squire-editor ol {
|
||||||
padding: 3px 5px;
|
list-style: revert;
|
||||||
font-size: 12px;
|
padding-left: 24px;
|
||||||
background: transparent;
|
margin: 4px 0;
|
||||||
border-color: transparent;
|
}
|
||||||
|
.squire-editor ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
.squire-editor ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
.squire-editor li {
|
||||||
|
margin: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-trix trix-toolbar .trix-button:hover,
|
.squire-editor blockquote {
|
||||||
.comment-trix trix-toolbar .trix-button.trix-active {
|
border-left: 3px solid #e2e8f0;
|
||||||
background: #e2e8f0;
|
color: #64748b;
|
||||||
color: #1e293b;
|
margin: 4px 0;
|
||||||
}
|
padding-left: 10px;
|
||||||
|
|
||||||
.comment-trix trix-toolbar .trix-button-group {
|
|
||||||
border-color: #e2e8f0;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-toolbar {
|
.comment-toolbar {
|
||||||
@@ -414,26 +453,37 @@ td {
|
|||||||
width: 14px;
|
width: 14px;
|
||||||
}
|
}
|
||||||
/* Trix content in activity items */
|
/* Trix content in activity items */
|
||||||
.activity-item .trix-content {
|
.activity-item .rich-content {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
.activity-item .trix-content div {
|
.activity-item .rich-content div {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.activity-item .trix-content strong {
|
.activity-item .rich-content strong {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.activity-item .trix-content a {
|
.activity-item .rich-content a {
|
||||||
color: #1d4ed8;
|
color: #1d4ed8;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.activity-item .trix-content ul,
|
.activity-item .rich-content ul,
|
||||||
.activity-item .trix-content ol {
|
.activity-item .rich-content ol {
|
||||||
padding-left: 16px;
|
padding-left: 24px;
|
||||||
margin: 4px 0;
|
list-style: revert;
|
||||||
}
|
}
|
||||||
.activity-item .trix-content blockquote {
|
.activity-item .rich-content ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
.activity-item .rich-content ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
.activity-item .rich-content ul,
|
||||||
|
.activity-item .rich-content ol,
|
||||||
|
.activity-item .rich-content li {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
.activity-item .rich-content blockquote {
|
||||||
border-left: 3px solid #e2e8f0;
|
border-left: 3px solid #e2e8f0;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
|||||||
@@ -22,9 +22,8 @@
|
|||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/trix@2/dist/trix.css">
|
<script src="https://cdn.jsdelivr.net/npm/squire-rte@1/build/squire-raw.js"></script>
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/trix@2/dist/trix.umd.min.js" defer></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-slate-50 text-slate-950">
|
<body class="min-h-screen bg-slate-50 text-slate-950">
|
||||||
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
||||||
|
|||||||
Reference in New Issue
Block a user