Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
384c80e34d | ||
|
|
ee60b1e5ac | ||
|
|
2e5704a615 | ||
|
|
d4fb9a0349 |
98
.gitea/workflows/deploy-ziwei.yml
Normal file
98
.gitea/workflows/deploy-ziwei.yml
Normal file
@@ -0,0 +1,98 @@
|
||||
name: Deploy ziwei-power
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy-ziwei:
|
||||
runs-on: prod-deploy
|
||||
env:
|
||||
DEPLOY_BASE: /opt/ziwei-power
|
||||
REPO_URL: https://qiukai:${{ secrets.DEPLOY_TOKEN }}@git.qiukai.me/qiukai/ziwei-power.git
|
||||
SERVICE_NAME: ziwei-power
|
||||
steps:
|
||||
- name: Clone and deploy
|
||||
run: |
|
||||
set -e
|
||||
RELEASE_ID="$(date +%Y%m%d-%H%M%S)"
|
||||
RELEASE_DIR="${DEPLOY_BASE}/releases/${RELEASE_ID}"
|
||||
CLONE_DIR="/tmp/zw-deploy-${RELEASE_ID}"
|
||||
|
||||
echo "=== 1. Clone repository ==="
|
||||
rm -rf "${CLONE_DIR}"
|
||||
git clone --depth 1 --branch main "${REPO_URL}" "${CLONE_DIR}"
|
||||
|
||||
echo "=== 2. Prepare release directory ==="
|
||||
rm -rf "${RELEASE_DIR}"
|
||||
mkdir -p "${RELEASE_DIR}"
|
||||
|
||||
rsync -a --exclude='.git' --exclude='.env' --exclude='.env.local' --exclude='.venv' --exclude='data/' --exclude='__pycache__' --exclude='.gitea' "${CLONE_DIR}/" "${RELEASE_DIR}/"
|
||||
|
||||
echo "=== 3. Link shared resources ==="
|
||||
mkdir -p "${DEPLOY_BASE}/shared"
|
||||
mkdir -p "${RELEASE_DIR}/data"
|
||||
ln -sfn "${DEPLOY_BASE}/shared/.env" "${RELEASE_DIR}/.env"
|
||||
DB_DIR="$HOME/.workbuddy/data/ziwei-power"
|
||||
mkdir -p "${DB_DIR}"
|
||||
ln -sfn "${DB_DIR}" "${RELEASE_DIR}/data"
|
||||
|
||||
echo "=== 4. Setup Python venv ==="
|
||||
cd "${RELEASE_DIR}"
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
echo "=== 5. Setup systemd (first run only) ==="
|
||||
if ! systemctl is-enabled "${SERVICE_NAME}" >/dev/null 2>&1; then
|
||||
cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<SVCEOF
|
||||
[Unit]
|
||||
Description=ziwei-power 日课系统
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$USER
|
||||
WorkingDirectory=${DEPLOY_BASE}/current
|
||||
ExecStart=${DEPLOY_BASE}/current/.venv/bin/python ${DEPLOY_BASE}/current/app.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=PORT=5058
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SVCEOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable "${SERVICE_NAME}"
|
||||
fi
|
||||
|
||||
echo "=== 6. Switch and restart ==="
|
||||
ln -sfn "${RELEASE_DIR}" "${DEPLOY_BASE}/current"
|
||||
systemctl restart "${SERVICE_NAME}"
|
||||
sleep 3
|
||||
|
||||
echo "=== 7. Health check ==="
|
||||
for i in 1 2 3 4 5; do
|
||||
if curl -fsS http://127.0.0.1:5058/api/health >/dev/null 2>&1; then
|
||||
echo "Health check passed"
|
||||
break
|
||||
fi
|
||||
echo "Attempt $i: waiting..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if ! curl -fsS http://127.0.0.1:5058/api/health >/dev/null 2>&1; then
|
||||
echo "ERROR: Health check failed"
|
||||
PREV=$(ls -t "${DEPLOY_BASE}/releases" | sed -n '2p')
|
||||
if [ -n "${PREV}" ]; then
|
||||
ln -sfn "${DEPLOY_BASE}/releases/${PREV}" "${DEPLOY_BASE}/current"
|
||||
systemctl restart "${SERVICE_NAME}"
|
||||
echo "Rolled back to ${PREV}"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== 8. Cleanup ==="
|
||||
ls -dt "${DEPLOY_BASE}"/releases/*/ | tail -n +6 | xargs -r rm -rf
|
||||
rm -rf "${CLONE_DIR}"
|
||||
|
||||
echo "=== Deploy complete: ${RELEASE_ID} ==="
|
||||
@@ -6,7 +6,7 @@ applyUserTenants().then(() => {
|
||||
return load();
|
||||
}).then(() => {
|
||||
const savedTab = localStorage.getItem("opc-active-tab");
|
||||
const validTabs = ["plan", "finance", "projects", "proposals", "products", "performance", "overview_plan", "overview_finance"];
|
||||
const validTabs = ["plan", "finance", "projects", "proposals", "products", "my", "performance", "overview_plan", "overview_finance"];
|
||||
if (savedTab && validTabs.includes(savedTab) && document.getElementById(savedTab)) {
|
||||
switchTab(savedTab);
|
||||
} else {
|
||||
|
||||
@@ -20,7 +20,7 @@ function openDrawer(resource, id) {
|
||||
const fields = resource === "sales"
|
||||
? [["target_customer","业务机会"],["priority","优先级"],["status","状态"]]
|
||||
: resource === "operations"
|
||||
? [["project_name","项目名称"],["owner","负责人"],["expected_sign_date","截止时间"],["expected_contract_amount","金额"],["notes","项目说明"]]
|
||||
? [["project_name","项目名称"],["notes","项目说明"]]
|
||||
: resource === "proposals"
|
||||
? [["customer_or_project_name","客户/项目"],["proposal_type","方案类型"],["notes","备注"]]
|
||||
: [["product_name","版本名称"],["version","版本号"],["priority","优先级"],["version_goal","版本目标"],["start_date","启动时间"],["plan_date","产品方案"],["dev_done_date","研发完成"],["test_date","测试完成"],["launch_date","上线时间"],["notes","进展备注"]];
|
||||
@@ -36,7 +36,7 @@ function openDrawer(resource, id) {
|
||||
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 followupTarget = resource === "sales" ? "sales" : resource === "proposals" ? "proposal" : 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">
|
||||
|
||||
221
static/modules/my.js
Normal file
221
static/modules/my.js
Normal file
@@ -0,0 +1,221 @@
|
||||
// my.js — 我的模块(绩效 + 预算月报 + 已发生月报)
|
||||
window.renderMy = () => {
|
||||
const data = state.data;
|
||||
if (!data) return;
|
||||
|
||||
// 确保数据已加载
|
||||
if (!data._myDataLoaded) {
|
||||
loadMyData().then(() => { data._myDataLoaded = true; renderMy(); });
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 状态初始化 ──
|
||||
if (state.planYear == null) state.planYear = new Date().getFullYear();
|
||||
const planYear = state.planYear;
|
||||
if (!['annual','quarterly','monthly'].includes(state.perfView)) state.perfView = 'quarterly';
|
||||
if (state.perfQuarter === undefined) state.perfQuarter = Math.floor(new Date().getMonth() / 3);
|
||||
const nowD = new Date();
|
||||
const activeQ = state.perfQuarter;
|
||||
const qRanges = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]];
|
||||
const activeMonths = qRanges[activeQ];
|
||||
const qLabels = ['Q1','Q2','Q3','Q4'];
|
||||
|
||||
let aggMonths = [];
|
||||
if (state.perfView === 'annual') { for (var mi = 1; mi <= 12; mi++) aggMonths.push(mi); }
|
||||
else if (state.perfView === 'quarterly') { aggMonths = qRanges[activeQ]; }
|
||||
else if (state.perfView === 'monthly') { const m = state.perfMonth; aggMonths = m ? [parseInt(m.substring(5))] : []; }
|
||||
|
||||
if (state.perfView === 'monthly' && !state.perfMonth) {
|
||||
state.perfMonth = planYear + '-' + String(nowD.getMonth() + 1).padStart(2, '0');
|
||||
}
|
||||
|
||||
const pfs = (data.projectFinances || []);
|
||||
const planPfs = (data.planFinances || []);
|
||||
|
||||
// ── 1. 绩效 ──
|
||||
function sumByMonths(items, field, useSign, months) {
|
||||
let total = 0;
|
||||
items.forEach(p => {
|
||||
if (useSign) {
|
||||
const sm = p.sign_month || '';
|
||||
const smY = parseInt(sm.substring(0, 4)) || 0;
|
||||
const smM = parseInt(sm.substring(5)) || 0;
|
||||
if (!months.includes(smM)) return;
|
||||
if (planYear !== 0 && smY && smY !== planYear) return;
|
||||
total += parseFloat(p.sign_amount || 0);
|
||||
} else {
|
||||
let bd = [];
|
||||
try { bd = JSON.parse(p.budget_data || '[]'); } catch (e) {}
|
||||
bd.forEach(b => {
|
||||
const m = parseInt((b.month || '').substring(5)) || 0;
|
||||
const y = parseInt((b.month || '').substring(0, 4)) || 0;
|
||||
if (!months.includes(m)) return;
|
||||
if (planYear !== 0 && y && y !== planYear) return;
|
||||
total += parseFloat(b[field] || 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
return Math.round(total);
|
||||
}
|
||||
|
||||
const perfRows = [
|
||||
{ key: 'sign', label: '签约', complete: sumByMonths(pfs, '', true, aggMonths), plan: sumByMonths(planPfs, '', true, aggMonths), icon: 'file-text', defWeight: 10 },
|
||||
{ key: 'rev', label: '收入', complete: sumByMonths(pfs, 'rev', false, aggMonths), plan: sumByMonths(planPfs, 'rev', false, aggMonths), icon: 'dollar-sign', defWeight: 20 },
|
||||
{ key: 'gross', label: '毛利', complete: sumByMonths(pfs, 'gross', false, aggMonths), plan: sumByMonths(planPfs, 'gross', false, aggMonths), icon: 'trending-up', defWeight: 50 },
|
||||
{ key: 'payment', label: '回款', complete: sumByMonths(pfs, 'payment', false, aggMonths), plan: sumByMonths(planPfs, 'payment', false, aggMonths), icon: 'coins', defWeight: 20 },
|
||||
];
|
||||
|
||||
let storageKey = 'opc-perf-t';
|
||||
if (state.perfView === 'quarterly') storageKey = 'opc-perf-Q' + (activeQ + 1);
|
||||
else if (state.perfView === 'monthly') storageKey = 'opc-perf-M' + (state.perfMonth || '');
|
||||
const saved = JSON.parse(localStorage.getItem(storageKey) || '{}');
|
||||
|
||||
function val(key, field, def) { const r = saved[key] || {}; return r[field] !== undefined && r[field] !== '' ? Number(r[field]) : def; }
|
||||
function moneyWan(v) { if (Math.abs(v) >= 10000) return (v / 10000).toFixed(1) + '万'; return v.toLocaleString(); }
|
||||
function moneyIntL(v) { return Math.round(Number(v || 0)).toLocaleString('zh-CN'); }
|
||||
|
||||
let totalScore = 0;
|
||||
const perfHTML = perfRows.map(r => {
|
||||
const target = r.plan; const weight = val(r.key, 'weight', r.defWeight);
|
||||
const score = target > 0 ? Math.round((r.complete / target) * weight * 10) / 10 : 0;
|
||||
const rate = target > 0 ? Math.round((r.complete / target) * 100) : 0;
|
||||
totalScore += score;
|
||||
return `<tr class="border-b border-slate-100">
|
||||
<td class="p-3 align-middle text-left font-medium text-slate-700"><i data-lucide="${r.icon}" style="width:16px;height:16px;display:inline-block;vertical-align:-3px;margin-right:6px" class="text-brand-500"></i>${r.label}</td>
|
||||
<td class="p-3 align-middle text-center text-slate-700 font-mono text-sm">${moneyWan(target)}</td>
|
||||
<td class="p-3 align-middle text-center text-slate-700 font-mono text-sm">${moneyWan(r.complete)}</td>
|
||||
<td class="p-3 align-middle text-center font-semibold text-sm ${rate >= 100 ? 'text-green-600' : rate >= 50 ? 'text-amber-600' : 'text-red-500'}">${rate}%</td>
|
||||
<td class="p-3 align-middle text-center"><div class="inline-flex justify-center"><input type="number" step="0.1" min="0" max="100" value="${weight}" class="perf-weight form-ctrl form-ctrl-sm w-16 text-center" style="appearance:none;-webkit-appearance:none" data-key="${r.key}" onchange="updateMyScore('${r.key}','weight',this.value)"></div></td>
|
||||
<td class="p-3 align-middle text-center font-semibold text-brand-700 text-sm">${score.toFixed(1)}</td></tr>`;
|
||||
}).join('');
|
||||
|
||||
// ── 绩效 Header ──
|
||||
const yearOpts = [2020,2021,2022,2023,2024,2025,2026,2027];
|
||||
const yearSelect = `<span class="text-sm text-slate-500">年份:</span><select onchange="setMyYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${planYear===0?' selected':''}>所有年份</option>${yearOpts.map(y => `<option value="${y}" ${planYear===y?'selected':''}>${y}年</option>`).join('')}</select>`;
|
||||
const viewBtns = `<button onclick="setMyView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.perfView==='annual'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全年</button>`;
|
||||
const quarterBtns = qLabels.map((ql,i) => `<button onclick="setMyQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.perfView==='quarterly'&&activeQ===i?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${ql}</button>`).join('');
|
||||
const monthBtns = activeMonths.map(m => { const ms = planYear+'-'+String(m).padStart(2,'0'); return `<button onclick="setMyMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.perfView==='monthly'&&state.perfMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('');
|
||||
const perfHeader = `<div class="card py-2 px-4"><div class="flex items-center gap-2 flex-wrap">${yearSelect}<span class="text-slate-300 mx-1">|</span>${viewBtns}<span class="text-slate-300 mx-1">|</span><span class="text-sm text-slate-500">季度:</span>${quarterBtns}<span class="text-slate-300 mx-1">|</span><span class="text-sm text-slate-500">月度:</span>${monthBtns}</div></div>`;
|
||||
|
||||
// ── 2. 预算月报 ──
|
||||
const planFm = computePlanMonthly(planPfs, planYear);
|
||||
const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
|
||||
const pLabels = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
||||
const pFields = ['sign','revenue','gross','cost','paid','payment',null];
|
||||
var pAnnual = { sign:0, revenue:0, gross:0, cost:0, paid:0, payment:0 };
|
||||
for (var i = 0; i < planFm.length; i++) { pAnnual.sign += planFm[i].sign; pAnnual.revenue += planFm[i].revenue; pAnnual.gross += planFm[i].gross; pAnnual.cost += planFm[i].cost; pAnnual.paid += planFm[i].paid; pAnnual.payment += planFm[i].payment; }
|
||||
var planThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var mi2 = 0; mi2 < 12; mi2++) { planThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + mLbl[mi2] + '</th>'; }
|
||||
planThead += '</tr>';
|
||||
var planTbody = '';
|
||||
for (var ri = 0; ri < pLabels.length; ri++) {
|
||||
var label = pLabels[ri];
|
||||
var fKey = pFields[ri];
|
||||
var annualVal = fKey ? pAnnual[fKey] : pAnnual.payment - pAnnual.paid;
|
||||
planTbody += '<tr class="border-b border-slate-100"><td class="py-1.5 text-slate-700 whitespace-nowrap">' + label + '</td><td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + moneyIntL(annualVal) + '</td>';
|
||||
for (var mi3 = 0; mi3 < 12; mi3++) {
|
||||
if (fKey) { planTbody += '<td class="py-1.5 text-right text-slate-600 whitespace-nowrap">' + moneyIntL(planFm[mi3][fKey]) + '</td>'; }
|
||||
else { planTbody += '<td class="py-1.5 text-right text-slate-600 whitespace-nowrap">' + moneyIntL(planFm[mi3].payment - planFm[mi3].paid) + '</td>'; }
|
||||
}
|
||||
planTbody += '</tr>';
|
||||
}
|
||||
var planOverviewHTML = `<section class="card p-4"><div class="overflow-x-auto"><table class="w-full text-xs plan-monthly-table"><thead>${planThead}</thead><tbody>${planTbody}</tbody></table></div></section>`;
|
||||
|
||||
// ── 3. 已发生月报 ──
|
||||
const finPfs = (data.projectFinances || []);
|
||||
const finFm = computePlanMonthly(finPfs, planYear);
|
||||
var fAnnual = { sign:0, revenue:0, gross:0, cost:0, paid:0, payment:0 };
|
||||
for (var fi = 0; fi < finFm.length; fi++) { fAnnual.sign += finFm[fi].sign; fAnnual.revenue += finFm[fi].revenue; fAnnual.gross += finFm[fi].gross; fAnnual.cost += finFm[fi].cost; fAnnual.paid += finFm[fi].paid; fAnnual.payment += finFm[fi].payment; }
|
||||
var finThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var mi4 = 0; mi4 < 12; mi4++) { finThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + mLbl[mi4] + '</th>'; }
|
||||
finThead += '</tr>';
|
||||
var finTbody = '';
|
||||
var fLabels2 = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
||||
for (var ri2 = 0; ri2 < fLabels2.length; ri2++) {
|
||||
var fKey2 = pFields[ri2];
|
||||
var aVal = fKey2 ? fAnnual[fKey2] : fAnnual.payment - fAnnual.paid;
|
||||
finTbody += '<tr class="border-b border-slate-100"><td class="py-1.5 text-slate-700 whitespace-nowrap">' + fLabels2[ri2] + '</td><td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + moneyIntL(aVal) + '</td>';
|
||||
for (var mi5 = 0; mi5 < 12; mi5++) {
|
||||
if (fKey2) { finTbody += '<td class="py-1.5 text-right text-slate-600 whitespace-nowrap">' + moneyIntL(finFm[mi5][fKey2]) + '</td>'; }
|
||||
else { finTbody += '<td class="py-1.5 text-right text-slate-600 whitespace-nowrap">' + moneyIntL(finFm[mi5].payment - finFm[mi5].paid) + '</td>'; }
|
||||
}
|
||||
finTbody += '</tr>';
|
||||
}
|
||||
var finOverviewHTML = `<section class="card p-4"><div class="overflow-x-auto"><table class="w-full text-xs plan-monthly-table"><thead>${finThead}</thead><tbody>${finTbody}</tbody></table></div></section>`;
|
||||
|
||||
// ── 组装页面 ──
|
||||
document.querySelector("#my").innerHTML = `
|
||||
<style>
|
||||
.perf-weight::-webkit-outer-spin-button,
|
||||
.perf-weight::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
||||
.perf-weight { -moz-appearance: textfield; }
|
||||
.plan-monthly-table td, .plan-monthly-table th { min-width: 70px; }
|
||||
.plan-monthly-table td:first-child, .plan-monthly-table th:first-child { min-width: 80px; }
|
||||
</style>
|
||||
<div class="grid gap-4">
|
||||
<h2 class="text-base font-bold text-slate-700 flex items-center gap-2"><i data-lucide="target" style="width:18px;height:18px" class="text-brand-500"></i>绩效</h2>
|
||||
${perfHeader}
|
||||
<section class="card p-4">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="bg-slate-50 border-b border-slate-200">
|
||||
<th class="p-3 text-left font-semibold text-slate-500">指标</th>
|
||||
<th class="p-3 text-center font-semibold text-slate-500">目标(万)</th>
|
||||
<th class="p-3 text-center font-semibold text-slate-500">完成情况</th>
|
||||
<th class="p-3 text-center font-semibold text-slate-500">完成率</th>
|
||||
<th class="p-3 text-center font-semibold text-slate-500">权重</th>
|
||||
<th class="p-3 text-center font-semibold text-slate-500">评分</th>
|
||||
</tr></thead>
|
||||
<tbody>${perfHTML}</tbody>
|
||||
<tfoot><tr class="border-t-2 border-slate-200 bg-slate-50 font-semibold">
|
||||
<td class="p-3 text-left text-slate-700">合计</td><td class="p-3 text-center text-slate-500">—</td><td class="p-3 text-center text-slate-500">—</td><td class="p-3 text-center text-slate-500">—</td><td class="p-3 text-center text-slate-700">100</td><td class="p-3 text-center text-brand-700 text-base">${totalScore.toFixed(1)}</td>
|
||||
</tr></tfoot>
|
||||
</table>
|
||||
</section>
|
||||
<h2 class="text-base font-bold text-slate-700 flex items-center gap-2 mt-4"><i data-lucide="calendar-range" style="width:18px;height:18px" class="text-brand-500"></i>预算月报</h2>
|
||||
${planOverviewHTML}
|
||||
<h2 class="text-base font-bold text-slate-700 flex items-center gap-2 mt-4"><i data-lucide="folder-open-dot" style="width:18px;height:18px" class="text-brand-500"></i>已发生月报</h2>
|
||||
${finOverviewHTML}
|
||||
<h2 class="text-base font-bold text-slate-700 flex items-center gap-2 mt-4"><i data-lucide="file-text" style="width:18px;height:18px" class="text-brand-500"></i>重点项目</h2>
|
||||
<section class="card p-4">
|
||||
${renderMyProjects()}
|
||||
</section>
|
||||
</div>`;
|
||||
|
||||
// ── 重点项目列表 ──
|
||||
function renderMyProjects() {
|
||||
const ops = state.data.operations || [];
|
||||
if (!ops.length) return '<p class="text-slate-400 text-sm py-4 text-center">暂无项目</p>';
|
||||
return '<div class="grid gap-2">' + ops.map(o => {
|
||||
return `<a href="javascript:void(0)" onclick="state.selectedProject=${o.id};switchTab('projects')" class="block p-2.5 rounded-lg hover:bg-slate-50 border border-slate-100 transition-colors">
|
||||
<span class="text-sm font-medium text-slate-700 truncate">${esc(o.project_name || '')}</span>
|
||||
</a>`;
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
};
|
||||
|
||||
// ── 筛选切换 ──
|
||||
window.setMyYear = (y) => { state.planYear = parseInt(y); renderMy(); };
|
||||
window.setMyView = (v) => { state.perfView = v; renderMy(); };
|
||||
window.setMyQuarter = (q) => { state.perfView = 'quarterly'; state.perfQuarter = q; renderMy(); };
|
||||
window.setMyMonth = (m) => { state.perfView = 'monthly'; state.perfMonth = m; renderMy(); };
|
||||
window.updateMyScore = (key, field, value) => {
|
||||
let storageKey = 'opc-perf-t';
|
||||
if (state.perfView === 'quarterly') storageKey = 'opc-perf-Q' + ((state.perfQuarter || 0) + 1);
|
||||
else if (state.perfView === 'monthly') storageKey = 'opc-perf-M' + (state.perfMonth || '');
|
||||
const saved = JSON.parse(localStorage.getItem(storageKey) || '{}');
|
||||
saved[key] = saved[key] || {};
|
||||
saved[key][field] = value;
|
||||
localStorage.setItem(storageKey, JSON.stringify(saved));
|
||||
renderMy();
|
||||
};
|
||||
|
||||
// ── 数据加载 ──
|
||||
async function loadMyData() {
|
||||
for (var i = 0; i < 3; i++) {
|
||||
var r = i === 0 ? 'planFinances' : i === 1 ? 'projectFinances' : 'operations';
|
||||
try { state.data[r] = await api("/api/" + r + "/list?tenant=" + encodeURIComponent(state.tenant)); }
|
||||
catch(e) { console.error('loadMyData failed:', r, e.message); }
|
||||
}
|
||||
}
|
||||
@@ -3,35 +3,93 @@ window.renderPerformance = () => {
|
||||
const data = state.data;
|
||||
if (!data) return;
|
||||
|
||||
// 确保预算和已发生数据已加载
|
||||
if (!data._perfDataLoaded) {
|
||||
loadPerfData().then(() => { data._perfDataLoaded = true; renderPerformance(); });
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 状态初始化 ──
|
||||
if (state.planYear == null) state.planYear = new Date().getFullYear();
|
||||
const planYear = state.planYear;
|
||||
if (!['annual','quarterly','monthly'].includes(state.perfView)) state.perfView = 'quarterly';
|
||||
if (state.perfQuarter === undefined) state.perfQuarter = Math.floor(new Date().getMonth() / 3);
|
||||
const q = state.perfQuarter;
|
||||
const qLabel = ['Q1 (1-3月)', 'Q2 (4-6月)', 'Q3 (7-9月)', 'Q4 (10-12月)'];
|
||||
const nowD = new Date();
|
||||
const activeQ = state.perfQuarter;
|
||||
|
||||
const qRanges = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]];
|
||||
const qRange = qRanges[q];
|
||||
const storageKey = 'opc-performance-Q' + (q + 1);
|
||||
const activeMonths = qRanges[activeQ];
|
||||
const qLabels = ['Q1','Q2','Q3','Q4'];
|
||||
|
||||
const pfs = (data.projectFinances || []);
|
||||
// 当前聚合范围
|
||||
let aggMonths = [];
|
||||
if (state.perfView === 'annual') {
|
||||
for (var mi = 1; mi <= 12; mi++) aggMonths.push(mi);
|
||||
} else if (state.perfView === 'quarterly') {
|
||||
aggMonths = qRanges[activeQ];
|
||||
} else if (state.perfView === 'monthly') {
|
||||
const m = state.perfMonth;
|
||||
aggMonths = m ? [parseInt(m.substring(5))] : [];
|
||||
}
|
||||
|
||||
const sumQ = (field) => {
|
||||
// 月度选择器默认值
|
||||
if (state.perfView === 'monthly' && !state.perfMonth) {
|
||||
state.perfMonth = planYear + '-' + String(nowD.getMonth() + 1).padStart(2, '0');
|
||||
}
|
||||
|
||||
// ── 数据源 ──
|
||||
const pfs = (data.projectFinances || []); // 已发生 → 完成情况
|
||||
const planPfs = (data.planFinances || []); // 预算 → 目标
|
||||
|
||||
// ── 通用聚合(支持年份过滤 + 月度范围)──
|
||||
function sumByMonths(items, field, useSign, months) {
|
||||
let total = 0;
|
||||
pfs.forEach(p => {
|
||||
try { JSON.parse(p.budget_data || '[]').forEach(b => { const m = parseInt((b.month || '').substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(b[field] || 0); }); } catch (e) {}
|
||||
items.forEach(p => {
|
||||
if (useSign) {
|
||||
const sm = p.sign_month || '';
|
||||
const smY = parseInt(sm.substring(0, 4)) || 0;
|
||||
const smM = parseInt(sm.substring(5)) || 0;
|
||||
if (!months.includes(smM)) return;
|
||||
if (planYear !== 0 && smY && smY !== planYear) return;
|
||||
total += parseFloat(p.sign_amount || 0);
|
||||
} else {
|
||||
let bd = [];
|
||||
try { bd = JSON.parse(p.budget_data || '[]'); } catch (e) {}
|
||||
bd.forEach(b => {
|
||||
const m = parseInt((b.month || '').substring(5)) || 0;
|
||||
const y = parseInt((b.month || '').substring(0, 4)) || 0;
|
||||
if (!months.includes(m)) return;
|
||||
if (planYear !== 0 && y && y !== planYear) return;
|
||||
total += parseFloat(b[field] || 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
return Math.round(total);
|
||||
};
|
||||
}
|
||||
|
||||
const signTotal = Math.round(pfs.filter(x => { const m = parseInt((x.sign_month || '0').substring(5)) || 0; return qRange.includes(m); }).reduce((a, p) => a + (p.sign_amount || 0), 0));
|
||||
const revTotal = sumQ('rev');
|
||||
const grossTotal = sumQ('gross');
|
||||
const paymentTotal = sumQ('payment');
|
||||
// ── 计算 ──
|
||||
const signActual = sumByMonths(pfs, '', true, aggMonths);
|
||||
const revActual = sumByMonths(pfs, 'rev', false, aggMonths);
|
||||
const grossActual = sumByMonths(pfs, 'gross', false, aggMonths);
|
||||
const paymentActual = sumByMonths(pfs, 'payment', false, aggMonths);
|
||||
|
||||
const signPlan = sumByMonths(planPfs, '', true, aggMonths);
|
||||
const revPlan = sumByMonths(planPfs, 'rev', false, aggMonths);
|
||||
const grossPlan = sumByMonths(planPfs, 'gross', false, aggMonths);
|
||||
const paymentPlan = sumByMonths(planPfs, 'payment', false, aggMonths);
|
||||
|
||||
const rows = [
|
||||
{ key: 'sign', label: '签约', complete: signTotal, icon: 'file-text', defWeight: 10 },
|
||||
{ key: 'rev', label: '收入', complete: revTotal, icon: 'dollar-sign', defWeight: 20 },
|
||||
{ key: 'gross', label: '毛利', complete: grossTotal, icon: 'trending-up', defWeight: 50 },
|
||||
{ key: 'payment', label: '回款', complete: paymentTotal, icon: 'coins', defWeight: 20 },
|
||||
{ key: 'sign', label: '签约', complete: signActual, plan: signPlan, icon: 'file-text', defWeight: 10 },
|
||||
{ key: 'rev', label: '收入', complete: revActual, plan: revPlan, icon: 'dollar-sign', defWeight: 20 },
|
||||
{ key: 'gross', label: '毛利', complete: grossActual, plan: grossPlan, icon: 'trending-up', defWeight: 50 },
|
||||
{ key: 'payment', label: '回款', complete: paymentActual, plan: paymentPlan, icon: 'coins', defWeight: 20 },
|
||||
];
|
||||
|
||||
// 存储 key 基于当前视图模式
|
||||
let storageKey = 'opc-perf-t'; // default annual
|
||||
if (state.perfView === 'quarterly') storageKey = 'opc-perf-Q' + (activeQ + 1);
|
||||
else if (state.perfView === 'monthly') storageKey = 'opc-perf-M' + (state.perfMonth || '');
|
||||
|
||||
const saved = JSON.parse(localStorage.getItem(storageKey) || '{}');
|
||||
|
||||
function val(key, field, def) {
|
||||
@@ -40,27 +98,23 @@ window.renderPerformance = () => {
|
||||
}
|
||||
|
||||
function moneyWan(v) {
|
||||
if (v >= 10000) return (v / 10000).toFixed(1) + '万';
|
||||
if (Math.abs(v) >= 10000) return (v / 10000).toFixed(1) + '万';
|
||||
return v.toLocaleString();
|
||||
}
|
||||
|
||||
let totalScore = 0;
|
||||
const tableRows = rows.map(r => {
|
||||
const targetWan = val(r.key, 'targetWan', 0);
|
||||
const target = Math.round(targetWan * 10000);
|
||||
const target = r.plan;
|
||||
const weight = val(r.key, 'weight', r.defWeight);
|
||||
const score = target > 0 ? Math.round((r.complete / target) * weight * 10) / 10 : 0;
|
||||
const rate = target > 0 ? Math.round((r.complete / target) * 100) : 0;
|
||||
totalScore += score;
|
||||
|
||||
return `<tr class="border-b border-slate-100">
|
||||
<td class="p-3 align-middle text-left font-medium text-slate-700"><i data-lucide="${r.icon}" style="width:16px;height:16px;display:inline-block;vertical-align:-3px;margin-right:6px" class="text-brand-500"></i>${r.label}</td>
|
||||
<td class="p-3 align-middle text-center">
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<input type="number" step="0.1" min="0" value="${targetWan.toFixed(1)}" class="perf-target form-ctrl form-ctrl-sm w-20 text-center" style="appearance:none;-webkit-appearance:none" data-key="${r.key}" onchange="updatePerfScore('${r.key}','targetWan',this.value)">
|
||||
<span class="text-xs text-slate-400">万</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3 align-middle text-center text-slate-700 font-mono text-sm">${moneyWan(target)}</td>
|
||||
<td class="p-3 align-middle text-center text-slate-700 font-mono text-sm">${moneyWan(r.complete)}</td>
|
||||
<td class="p-3 align-middle text-center font-semibold text-sm ${rate >= 100 ? 'text-green-600' : rate >= 50 ? 'text-amber-600' : 'text-red-500'}">${rate}%</td>
|
||||
<td class="p-3 align-middle text-center">
|
||||
<div class="inline-flex justify-center">
|
||||
<input type="number" step="0.1" min="0" max="100" value="${weight}" class="perf-weight form-ctrl form-ctrl-sm w-16 text-center" style="appearance:none;-webkit-appearance:none" data-key="${r.key}" onchange="updatePerfScore('${r.key}','weight',this.value)">
|
||||
@@ -70,9 +124,27 @@ window.renderPerformance = () => {
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
const quarterTabs = `<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0">` + qLabel.map((l, i) => {
|
||||
return `<button onclick="switchPerfQuarter(${i})" class="finance-tab${i === q ? ' active' : ''}" style="font-size:14px;font-weight:600">${l}</button>`;
|
||||
}).join('') + `</div>`;
|
||||
// ── Header: 仿预算/已发生模块的年/季/月筛选区 ──
|
||||
const yearOpts = [2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027];
|
||||
const yearSelect = `<span class="text-sm text-slate-500">年份:</span><select onchange="setPerfYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${planYear===0?' selected':''}>所有年份</option>${yearOpts.map(y => `<option value="${y}" ${planYear===y?'selected':''}>${y}年</option>`).join('')}</select>`;
|
||||
|
||||
const viewBtns = `<button onclick="setPerfView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.perfView==='annual'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全年</button>`;
|
||||
|
||||
const quarterBtns = qLabels.map((ql, i) =>
|
||||
`<button onclick="setPerfQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.perfView==='quarterly'&&activeQ===i?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${ql}</button>`
|
||||
).join('');
|
||||
|
||||
const monthBtns = activeMonths.map(m => {
|
||||
const ms = planYear + '-' + String(m).padStart(2, '0');
|
||||
return `<button onclick="setPerfMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.perfView==='monthly'&&state.perfMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`;
|
||||
}).join('');
|
||||
|
||||
const headerBar = `<div class="flex items-center gap-2 flex-wrap">
|
||||
${yearSelect}<span class="text-slate-300 mx-1">|</span>
|
||||
${viewBtns}
|
||||
<span class="text-slate-300 mx-1">|</span><span class="text-sm text-slate-500">季度:</span>${quarterBtns}
|
||||
<span class="text-slate-300 mx-1">|</span><span class="text-sm text-slate-500">月度:</span>${monthBtns}
|
||||
</div>`;
|
||||
|
||||
document.querySelector("#performance").innerHTML = `
|
||||
<style>
|
||||
@@ -83,13 +155,14 @@ window.renderPerformance = () => {
|
||||
.perf-target, .perf-weight { -moz-appearance: textfield; }
|
||||
</style>
|
||||
<div class="grid gap-4">
|
||||
${quarterTabs}
|
||||
<div class="card py-2 px-4">${headerBar}</div>
|
||||
<section class="card p-4">
|
||||
<table class="w-full text-sm">
|
||||
<thead><tr class="bg-slate-50 border-b border-slate-200">
|
||||
<th class="p-3 text-left font-semibold text-slate-500">指标</th>
|
||||
<th class="p-3 text-center font-semibold text-slate-500">目标(万)</th>
|
||||
<th class="p-3 text-center font-semibold text-slate-500">完成情况</th>
|
||||
<th class="p-3 text-center font-semibold text-slate-500">完成率</th>
|
||||
<th class="p-3 text-center font-semibold text-slate-500">权重</th>
|
||||
<th class="p-3 text-center font-semibold text-slate-500">评分</th>
|
||||
</tr></thead>
|
||||
@@ -98,6 +171,7 @@ window.renderPerformance = () => {
|
||||
<td class="p-3 text-left text-slate-700">合计</td>
|
||||
<td class="p-3 text-center text-slate-500">—</td>
|
||||
<td class="p-3 text-center text-slate-500">—</td>
|
||||
<td class="p-3 text-center text-slate-500">—</td>
|
||||
<td class="p-3 text-center text-slate-700">100</td>
|
||||
<td class="p-3 text-center text-brand-700 text-base">${totalScore.toFixed(1)}</td>
|
||||
</tr></tfoot>
|
||||
@@ -108,17 +182,47 @@ window.renderPerformance = () => {
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
};
|
||||
|
||||
window.switchPerfQuarter = (q) => {
|
||||
// ── 筛选切换函数 ──
|
||||
window.setPerfYear = (y) => {
|
||||
state.planYear = parseInt(y);
|
||||
renderPerformance();
|
||||
};
|
||||
|
||||
window.setPerfView = (v) => {
|
||||
state.perfView = v;
|
||||
renderPerformance();
|
||||
};
|
||||
|
||||
window.setPerfQuarter = (q) => {
|
||||
state.perfView = 'quarterly';
|
||||
state.perfQuarter = q;
|
||||
renderPerformance();
|
||||
};
|
||||
|
||||
window.setPerfMonth = (m) => {
|
||||
state.perfView = 'monthly';
|
||||
state.perfMonth = m;
|
||||
renderPerformance();
|
||||
};
|
||||
|
||||
// ── 编辑保存 ──
|
||||
window.updatePerfScore = (key, field, value) => {
|
||||
const q = state.perfQuarter || 0;
|
||||
const storageKey = 'opc-performance-Q' + (q + 1);
|
||||
let storageKey = 'opc-perf-t';
|
||||
if (state.perfView === 'quarterly') storageKey = 'opc-perf-Q' + ((state.perfQuarter || 0) + 1);
|
||||
else if (state.perfView === 'monthly') storageKey = 'opc-perf-M' + (state.perfMonth || '');
|
||||
const saved = JSON.parse(localStorage.getItem(storageKey) || '{}');
|
||||
saved[key] = saved[key] || {};
|
||||
saved[key][field] = value;
|
||||
localStorage.setItem(storageKey, JSON.stringify(saved));
|
||||
renderPerformance();
|
||||
};
|
||||
|
||||
// ── 数据加载 ──
|
||||
async function loadPerfData() {
|
||||
for (var i = 0; i < 2; i++) {
|
||||
var r = i === 0 ? 'planFinances' : 'projectFinances';
|
||||
try {
|
||||
state.data[r] = await api("/api/" + r + "/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
} catch(e) { console.error('loadPerfData failed:', r, e.message); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// utils.js — 全局工具函数与共享状态
|
||||
|
||||
const state = {
|
||||
active: "plan",
|
||||
active: "my",
|
||||
data: null,
|
||||
tenant: localStorage.getItem("opc-active-tenant") || "科普·无界",
|
||||
opFilter: "all",
|
||||
@@ -126,6 +126,7 @@ var _loadingSteps = [
|
||||
{ fn: 'renderProposals', label: '正在加载业务方案列表' },
|
||||
{ fn: 'renderProducts', label: '正在加载产品迭代列表' },
|
||||
{ fn: 'renderPerformance', label: '正在加载绩效管理' },
|
||||
{ fn: 'renderMy', label: '正在加载我的模块' },
|
||||
{ fn: 'renderFinance', label: '正在加载已发生模块(表格+总览+图表)' },
|
||||
{ fn: 'renderPlan', label: '正在加载预算模块' },
|
||||
];
|
||||
@@ -234,7 +235,7 @@ var _tabLoaded = {};
|
||||
var _proposalsInitialized = false;
|
||||
|
||||
async function ensureTabData(tab) {
|
||||
if (tab === 'performance' || tab === 'overview_plan' || tab === 'overview_finance') return;
|
||||
if (tab === 'my' || tab === 'performance' || tab === 'overview_plan' || tab === 'overview_finance') return;
|
||||
var resource = _tabResourceMap[tab];
|
||||
if (!resource) return;
|
||||
var dataKey = resource;
|
||||
@@ -266,7 +267,7 @@ function switchTab(tab) {
|
||||
document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === tab));
|
||||
|
||||
// 更新顶部标题
|
||||
const titles = { finance: '执行管理', plan: '预算管理', projects: '重点工作台账', proposals: '业务方案', products: '版本与运营', performance: '季度绩效考核', overview_plan: '预算总览', overview_finance: '发生总览' };
|
||||
const titles = { finance: '执行管理', plan: '预算管理', projects: '重点工作台账', proposals: 'wiki', products: '版本与运营', performance: '季度绩效考核', my: '我的', overview_plan: '预算总览', overview_finance: '发生总览' };
|
||||
const titleEl = document.querySelector('#workspaceTitle');
|
||||
if (titleEl) titleEl.textContent = titles[tab] || state.tenant + ' OPC 工作台';
|
||||
|
||||
@@ -298,6 +299,7 @@ function renderActive() {
|
||||
else if (tab === "proposals") renderProposals();
|
||||
else if (tab === "products") renderProducts();
|
||||
else if (tab === "performance") renderPerformance();
|
||||
else if (tab === "my") renderMy();
|
||||
else if (tab === "finance") renderFinance();
|
||||
else if (tab === "plan") renderPlan();
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
|
||||
@@ -63,6 +63,10 @@
|
||||
<div class="w-8 h-px bg-slate-700 my-2"></div>
|
||||
<!-- 导航 Tab 图标 -->
|
||||
<div class="flex flex-col items-center gap-1 w-14" id="sidebarTabs">
|
||||
<div class="sidebar-tab" data-tab="my" onclick="switchTab('my')" title="我的">
|
||||
<i data-lucide="target" style="width:20px;height:20px"></i>
|
||||
<span class="text-[10px] mt-1">我的</span>
|
||||
</div>
|
||||
<div class="sidebar-tab" data-tab="plan" onclick="switchTab('plan')" title="预算">
|
||||
<i data-lucide="calendar-range" style="width:20px;height:20px"></i>
|
||||
<span class="text-[10px] mt-1">预算</span>
|
||||
@@ -83,17 +87,13 @@
|
||||
<i data-lucide="file-text" style="width:20px;height:20px"></i>
|
||||
<span class="text-[10px] mt-1">Focus</span>
|
||||
</div>
|
||||
<div class="sidebar-tab" data-tab="proposals" onclick="switchTab('proposals')" title="业务方案">
|
||||
<i data-lucide="package" style="width:20px;height:20px"></i>
|
||||
<span class="text-[10px] mt-1">方案</span>
|
||||
</div>
|
||||
<div class="sidebar-tab" data-tab="products" onclick="switchTab('products')" title="产品迭代">
|
||||
<i data-lucide="wallet-cards" style="width:20px;height:20px"></i>
|
||||
<span class="text-[10px] mt-1">产品</span>
|
||||
</div>
|
||||
<div class="sidebar-tab" data-tab="performance" onclick="switchTab('performance')" title="绩效">
|
||||
<i data-lucide="target" style="width:20px;height:20px"></i>
|
||||
<span class="text-[10px] mt-1">绩效</span>
|
||||
<div class="sidebar-tab" data-tab="proposals" onclick="switchTab('proposals')" title="wiki">
|
||||
<i data-lucide="package" style="width:20px;height:20px"></i>
|
||||
<span class="text-[10px] mt-1">wiki</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -123,9 +123,9 @@
|
||||
<section id="projects" class="panel"></section>
|
||||
<section id="proposals" class="panel"></section>
|
||||
<section id="products" class="panel"></section>
|
||||
<section id="performance" class="panel"></section>
|
||||
<section id="my" class="panel active"></section>
|
||||
<section id="finance" class="panel"></section>
|
||||
<section id="plan" class="panel active"></section>
|
||||
<section id="plan" class="panel"></section>
|
||||
</main>
|
||||
</div><!-- 关闭主内容区 -->
|
||||
</div><!-- 关闭 flex 容器 -->
|
||||
@@ -152,7 +152,7 @@
|
||||
<script src="{{ url_for('static', filename='modules/projects.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/proposals.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/products.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/performance.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/my.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/finance.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/expense.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='modules/plan.js') }}"></script>
|
||||
|
||||
Reference in New Issue
Block a user