// ==UserScript== // @name 任务团队分配实时看板(团队/人员双视角) // @namespace dsh.teamboard // @version 1.1.0 // @description 实时查看任务分配给了哪些团队、哪些人员,各工序承接与进度如何(纯只读 GET,不修改平台任何数据) // @author DSH // @match *://*/v/project/*/project-taskManage* // @include /^https?:\/\/[^/]+\/v\/project\/[^/]+\/project-taskManage/ // @grant none // @run-at document-idle // @noframes // ==/UserScript== /* * 通用版说明 * ───────────────────────────────────────────────────────────── * @match 使用 *://* 通配,因此**任意域名、任意端口**部署的同一套平台都能直接生效, * 不再绑定某个固定 IP。项目标识由当前网址自动解析,接口全部走相对路径。 * * 只调用平台自身的只读查询接口(全部是 HTTP GET): * GET /v2/projects/{project_key}/task-batches 任务批次列表(含 allocation_info 分配信息) * GET /v2/guilds 团队(公会)列表 * 鉴权复用页面登录态:localStorage.token → 请求头 access-key。 * 不会发起任何 POST / PUT / DELETE,不会改动平台数据。 * * 如果要接入的是**另一家标注平台**(接口与字段完全不同),请改用同目录的浏览器 * 扩展版本:它把站点匹配、鉴权、接口路径、字段映射都做成了可视化配置的「站点档案」, * 不需要改代码。本用户脚本面向的是 /v2 REST 风格的这套平台。 * ───────────────────────────────────────────────────────────── */ (function () { 'use strict'; if (window.__DSH_TEAM_BOARD_LOADED__) return; window.__DSH_TEAM_BOARD_LOADED__ = true; /* ============================ 配置 ============================ */ var CFG_KEY = 'dsh_team_board_cfg_v1'; var cfg = Object.assign({ autoRefresh: true, intervalSec: 15, inlineBadge: true, panelOpen: true, panelW: 1000, panelH: 620, fontScale: 1.15, panelX: null, panelY: null, tab: 'task', keyword: '' }, safeParse(localStorage.getItem(CFG_KEY))); function saveCfg() { try { localStorage.setItem(CFG_KEY, JSON.stringify(cfg)); } catch (e) {} } function safeParse(s) { try { return s ? JSON.parse(s) : null; } catch (e) { return null; } } /* ============================ 常量 ============================ */ var WORK_TYPES = { '1': '标注', '2': '审核', '3': '质检', '4': '验收' }; var WORK_ORDER = ['1', '2', '3', '4']; var WORK_COLOR = { '1': '#409eff', '2': '#e6a23c', '3': '#909399', '4': '#67c23a' }; var TASK_TYPES = { 704: '音频标注', 902: '点云标注', 1001: '图像通用标注', 1002: 'OCR文字转写', 1003: 'REID目标跟踪', 1004: '图像语义分割', 1006: '人体关键点', 1103: '文本生成', 1201: '视频标注', 1202: '命名实体识别与关系标注', 1111: '意图及实体标注', 1112: '文章判断', 1113: '相似文本判断' }; /* ============================ 状态 ============================ */ var state = { loading: false, lastUpdate: null, error: '', tasks: [], // 归一化后的任务 taskMap: {}, // task_key -> task guilds: [], // 团队列表 guildMap: {}, // gid -> guild_name users: [], // 人员列表 userMap: {}, // user_id -> {account, nickname} source: '', // 数据来源:api / page rawSample: null }; var refreshTimer = null; var domObserver = null; var badgeTimer = null; var currentPath = location.pathname; /* ============================ 工具 ============================ */ function esc(s) { return String(s === undefined || s === null ? '' : s) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function num(v) { var n = Number(v); return isFinite(n) ? n : 0; } function pad2(n) { return n < 10 ? '0' + n : '' + n; } function fmtTime(d) { if (!d) return '—'; return pad2(d.getHours()) + ':' + pad2(d.getMinutes()) + ':' + pad2(d.getSeconds()); } function projectKey() { var m = location.pathname.match(/\/v\/project\/([^/]+)\//); return m ? m[1] : ''; } function getToken() { try { var raw = localStorage.getItem('token'); if (!raw) return ''; return String(raw).replace(/^"|"$/g, ''); } catch (e) { return ''; } } function getUserInfo() { try { return JSON.parse(localStorage.getItem('userInfo')) || {}; } catch (e) { return {}; } } function isOk(res) { if (!res) return false; return res.code === '' || res.code === 0 || res.code === '0'; } /* ============================ API(只读 GET) ============================ */ function apiGet(path, params) { var qs = ''; if (params) { var parts = []; Object.keys(params).forEach(function (k) { var v = params[k]; if (v === undefined || v === null) return; parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(v)); }); if (parts.length) qs = (path.indexOf('?') > -1 ? '&' : '?') + parts.join('&'); } var url = path + qs; var headers = { 'access-key': getToken(), 'time': String(Date.now()) }; return fetch(url, { method: 'GET', credentials: 'same-origin', headers: headers, cache: 'no-store' }).then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }); } /* ============================ 数据加载 ============================ */ function orgId() { var u = getUserInfo(); return u && u.organize_id ? u.organize_id : ''; } /** * 分页拉取全部任务批次(服务端可能限制单页条数,因此按 meta.total_num 翻页)。 * 只读 GET,不会修改任何数据。 */ function fetchAllTasks(key) { var PAGE_SIZE = 200; var MAX_PAGES = 60; // 上限 12000 条,避免异常时无限翻页 var collected = []; var seen = {}; var first = null; function one(page) { return apiGet('/v2/projects/' + encodeURIComponent(key) + '/task-batches', { task_name: '', task_key: '', line_status: '', page: page, page_num: PAGE_SIZE, creator_ids: '', file_name: '', seg_dir_name: '' }); } function step(page) { return one(page).then(function (res) { if (!isOk(res)) { if (first) return first; // 翻页中途失败:返回已拿到的部分 return res; // 首页失败:交给上层报错 } if (!first) first = res; var items = (res.data && res.data.items) || []; // 服务端可能把 page_num 截断到一个更小的上限,因此用「新增条数」而不是请求条数判断结束 var added = 0; items.forEach(function (it) { var k = it.task_batch_key || it.task_info_id || ('#' + collected.length); if (seen[k]) return; seen[k] = 1; collected.push(it); added++; }); var total = num(res.data && res.data.meta && res.data.meta.total_num); var noMore = items.length === 0 || added === 0 || page >= MAX_PAGES || (total > 0 ? collected.length >= total : items.length < PAGE_SIZE); if (noMore) { var out = JSON.parse(JSON.stringify(first)); out.data = out.data || {}; out.data.items = collected; out.__items = collected; return out; } return step(page + 1); }); } return step(1); } function loadAll() { var key = projectKey(); if (!key) return Promise.resolve(); state.loading = true; state.error = ''; render(); var pTasks = fetchAllTasks(key); var pGuilds = apiGet('/v2/guilds', { page: 1, page_num: 99999, guild_name: '', organize_id: orgId() }).catch(function () { return null; }); // 人员列表:user_items 里一般只有 user_id,需要靠它把「账号」查出来 var pUsers = apiGet('/v2/users', { page: 1, page_num: 9999, user_name: '', nickname: '', status: '', organize_id: orgId() }).catch(function () { return null; }); return Promise.all([pTasks, pGuilds, pUsers]).then(function (arr) { var taskRes = arr[0]; var guildRes = arr[1]; var userRes = arr[2]; if (!isOk(taskRes)) { throw new Error((taskRes && taskRes.msg) ? taskRes.msg : '任务列表接口返回异常'); } var items = taskRes.__items || (taskRes.data && taskRes.data.items) || []; state.rawSample = items[0] || null; state.tasks = items.map(normalizeTask); state.taskMap = {}; state.tasks.forEach(function (t) { if (t.key) state.taskMap[t.key] = t; }); if (isOk(guildRes)) { state.guilds = (guildRes.data && guildRes.data.items) || []; state.guildMap = {}; state.guilds.forEach(function (g) { state.guildMap[String(g.gid)] = g.guild_name; }); } if (isOk(userRes)) { state.users = (userRes.data && userRes.data.items) || []; state.userMap = {}; state.users.forEach(function (u) { state.userMap[String(u.user_id)] = { account: u.user_name, nickname: u.nickname }; }); } state.source = 'api'; state.lastUpdate = new Date(); state.loading = false; state.error = ''; render(); injectBadges(); }).catch(function (err) { state.loading = false; state.error = (err && err.message) ? err.message : String(err); render(); }); } function normalizeTask(t) { var access = {}; (t.access_info || []).forEach(function (a) { if (a && a.work_type !== undefined) access[String(a.work_type)] = a.access; }); var alloc = t.allocation_info || {}; var teamsByWt = {}; var usersByWt = {}; (alloc.guild_items || []).forEach(function (g) { var wt = String(g.work_type); (teamsByWt[wt] = teamsByWt[wt] || []).push(g); }); (alloc.user_items || []).forEach(function (u) { var wt = String(u.work_type); (usersByWt[wt] = usersByWt[wt] || []).push(u); }); var accept = num(t.accept_num), check = num(t.check_num), inspect = num(t.inspect_num), done = num(t.done_num); var total = num(t.total_num); return { key: t.task_batch_key, name: t.task_batch_name, typeCode: t.task_type, type: TASK_TYPES[t.task_type] || ('类型' + t.task_type), total: total, accept: accept, check: check, inspect: inspect, done: done, progress: { '1': accept + check + inspect + done, '2': accept + inspect + done, '3': accept + done, '4': done }, online: String(t.online_status) === '1', access: access, teamsByWt: teamsByWt, usersByWt: usersByWt, creator: t.creator_nickname, createdAt: t.created_at, raw: t }; } /* 某工序是否启用 */ function wtEnabled(task, wt) { if (!task.access) return false; return Object.prototype.hasOwnProperty.call(task.access, wt); } /* 该任务实际涉及的工序(有启用、或有团队/个人被分配) */ function activeWts(task) { var set = {}; WORK_ORDER.forEach(function (wt) { if (wtEnabled(task, wt) || (task.teamsByWt[wt] && task.teamsByWt[wt].length) || (task.usersByWt[wt] && task.usersByWt[wt].length)) set[wt] = true; }); var out = WORK_ORDER.filter(function (wt) { return set[wt]; }); return out.length ? out : WORK_ORDER.slice(0, 1); } function teamNames(task, wt) { var list = task.teamsByWt[wt] || []; return list.map(function (g) { return g.guild_name || state.guildMap[String(g.gid)] || ('团队' + g.gid); }); } function hasAnyTeam(task) { return WORK_ORDER.some(function (wt) { return (task.teamsByWt[wt] || []).length > 0; }); } /* ---- 人员维度 ---- */ function userIdOf(u) { return String(u.user_id === undefined || u.user_id === null ? '' : u.user_id); } /** 账号 / 昵称:条目自带 → 人员列表按 user_id 回填 → 空 */ function userInfoOf(u) { var uid = userIdOf(u); var rec = state.userMap[uid] || {}; var account = u.user_name || u.username || u.account || rec.account; var nick = u.nickname || u.nick_name || u.real_name || rec.nickname; return { uid: uid, account: (account === undefined || account === null) ? '' : String(account), nickname: (nick === undefined || nick === null) ? '' : String(nick) }; } /** 展示用:账号优先,有昵称且不同则括号补充,都没有才退回 用户{id} */ function userLabelOf(u) { var i = userInfoOf(u); if (i.account && i.nickname && i.account !== i.nickname) return i.account + '(' + i.nickname + ')'; if (i.account) return i.account; if (i.nickname) return i.nickname; return '用户' + i.uid; } function userNames(task, wt) { return (task.usersByWt[wt] || []).map(userLabelOf); } function hasAnyUser(task) { return WORK_ORDER.some(function (wt) { return (task.usersByWt[wt] || []).length > 0; }); } /** 团队或个人任一维度有分配 */ function hasAnyAssignee(task) { return hasAnyTeam(task) || hasAnyUser(task); } /** 某工序既无团队也无个人 */ function wtEmptyOf(task, wt) { return teamNames(task, wt).length === 0 && userNames(task, wt).length === 0; } /* ============================ 面板 UI ============================ */ var host = null, root = null, shadow = null, bodyEl = null; function ensurePanel() { if (host && document.body.contains(host)) return; host = document.createElement('div'); host.id = 'dsh-team-board-host'; host.style.cssText = 'all:initial;position:fixed;z-index:2147483000;'; document.body.appendChild(host); shadow = host.attachShadow({ mode: 'open' }); shadow.innerHTML = [ '', '
', '
', ' ', ' 任务团队分配看板', ' ', ' ', ' ', ' ', '
', '
', '
', '
', '
', ' ', '
', '
' ].join('\n'); bodyEl = shadow.getElementById('body'); applyGeom(); shadow.getElementById('btnRefresh').addEventListener('click', function () { loadAll(); }); shadow.getElementById('btnCsv').addEventListener('click', exportCsv); shadow.getElementById('btnFold').addEventListener('click', function () { cfg.panelOpen = !cfg.panelOpen; saveCfg(); applyGeom(); render(); }); makeDraggable(shadow.getElementById('hd'), shadow.getElementById('wrap')); render(); } function applyGeom() { if (!shadow) return; var w = Math.max(420, Math.min(cfg.panelW, window.innerWidth - 20)); var h = Math.max(160, Math.min(cfg.panelH, window.innerHeight - 20)); var x = cfg.panelX === null ? Math.max(10, window.innerWidth - w - 18) : cfg.panelX; var y = cfg.panelY === null ? 70 : cfg.panelY; x = Math.max(0, Math.min(x, window.innerWidth - w)); y = Math.max(0, Math.min(y, window.innerHeight - 40)); var base = 13 * (cfg.fontScale || 1.15); var wrap = shadow.getElementById('wrap'); wrap.style.fontSize = base + 'px'; // 全局字号,其余尺寸都是 em wrap.style.width = w + 'px'; wrap.style.height = (cfg.panelOpen ? h : Math.round(base * 2.9)) + 'px'; wrap.style.left = x + 'px'; wrap.style.top = y + 'px'; wrap.classList.toggle('collapsed', !cfg.panelOpen); shadow.getElementById('btnFold').textContent = cfg.panelOpen ? '收起' : '展开'; } function makeDraggable(handle, target) { var sx = 0, sy = 0, ox = 0, oy = 0, dragging = false; handle.addEventListener('mousedown', function (e) { if (e.target.tagName === 'BUTTON') return; dragging = true; sx = e.clientX; sy = e.clientY; ox = parseInt(target.style.left, 10) || 0; oy = parseInt(target.style.top, 10) || 0; e.preventDefault(); }); window.addEventListener('mousemove', function (e) { if (!dragging) return; var nx = Math.max(0, Math.min(ox + e.clientX - sx, window.innerWidth - 80)); var ny = Math.max(0, Math.min(oy + e.clientY - sy, window.innerHeight - 40)); target.style.left = nx + 'px'; target.style.top = ny + 'px'; cfg.panelX = nx; cfg.panelY = ny; }); window.addEventListener('mouseup', function () { if (!dragging) return; dragging = false; saveCfg(); }); } /* ============================ 渲染 ============================ */ function render() { if (!shadow) return; var dot = shadow.getElementById('dot'); dot.className = 'dot' + (state.error ? ' err' : (state.loading ? ' load' : '')); renderBar(); renderMeta(); renderBody(); renderFoot(); } var barSig = null; function renderBar() { var bar = shadow.getElementById('bar'); var tabs = [['task', '任务视角'], ['team', '团队视角'], ['user', '人员视角'], ['unassigned', '未分配'], ['raw', '诊断']]; var sig = cfg.tab + '|' + cfg.keyword; // 结构未变化时不重建,避免自动刷新打断正在输入的搜索框 if (barSig !== sig) { var focused = shadow.activeElement && shadow.activeElement.id === 'srch'; var caret = focused ? shadow.activeElement.selectionStart : 0; var html = tabs.map(function (t) { return '' + t[1] + ''; }).join(''); html += ''; bar.innerHTML = html; barSig = sig; bar.querySelectorAll('.tab').forEach(function (el) { el.addEventListener('click', function () { cfg.tab = el.getAttribute('data-tab'); saveCfg(); render(); }); }); var srch = bar.querySelector('#srch'); srch.addEventListener('input', function () { cfg.keyword = srch.value; barSig = null; saveCfg(); renderBody(); }); if (focused) { srch.focus(); try { srch.setSelectionRange(caret, caret); } catch (e) {} } } } function renderMeta() { var meta = shadow.getElementById('meta'); var parts = []; parts.push('项目:' + esc(projectKey() || '—') + ''); parts.push('任务:' + state.tasks.length + ''); parts.push('团队:' + state.guilds.length + ''); parts.push('人员:' + state.users.length + ''); parts.push('更新:' + fmtTime(state.lastUpdate) + ''); if (state.loading) parts.push('加载中…'); if (state.error) parts.push('错误:' + esc(state.error) + ''); meta.innerHTML = parts.join(''); } function matchKeyword(txt) { if (!cfg.keyword) return true; return String(txt).toLowerCase().indexOf(cfg.keyword.toLowerCase()) > -1; } function renderBody() { var scroll = shadow.getElementById('scroll'); if (cfg.tab === 'task') scroll.innerHTML = renderTaskTable(); else if (cfg.tab === 'team') scroll.innerHTML = renderTeamTable(); else if (cfg.tab === 'user') scroll.innerHTML = renderUserTable(); else if (cfg.tab === 'unassigned') scroll.innerHTML = renderUnassigned(); else scroll.innerHTML = renderRaw(); } function userCell(task) { var wts = activeWts(task); if (!hasAnyUser(task)) return '全部按团队分配'; return wts.map(function (wt) { var c = WORK_COLOR[wt] || '#9fb2cd'; var names = userNames(task, wt); var head = '' + WORK_TYPES[wt] + ''; if (!names.length) return head + '—'; return head + names.map(function (n) { return '' + esc(n) + ''; }).join(''); }).join('
'); } function progressCell(task) { var wts = activeWts(task); return wts.map(function (wt) { var done = task.progress[wt] || 0; var color = WORK_COLOR[wt] || '#9fb2cd'; return '' + WORK_TYPES[wt] + ' ' + done + '/' + task.total + ''; }).join(''); } function teamCell(task) { var wts = activeWts(task); var out = []; wts.forEach(function (wt) { var names = teamNames(task, wt); var color = WORK_COLOR[wt] || '#9fb2cd'; var label = '' + WORK_TYPES[wt] + ''; if (names.length) { out.push(label + names.map(function (n) { return '' + esc(n) + ''; }).join('')); } else { out.push(label + '未分配'); } }); return out.join('
'); } function renderTaskTable() { var rows = state.tasks.filter(function (t) { return matchKeyword(t.name) || matchKeyword(t.key) || matchKeyword(t.type) || activeWts(t).some(function (wt) { return teamNames(t, wt).some(matchKeyword) || userNames(t, wt).some(matchKeyword); }); }); if (!rows.length) return '
' + (state.tasks.length ? '没有匹配的任务' : '暂无数据(点“刷新”重试)') + '
'; var h = '' + '' + ''; rows.forEach(function (t) { var cls = (hasAnyAssignee(t) ? '' : 'unassigned') + (t.online ? '' : ' offline'); h += '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; }); return h + '
任务名称task_key标注类型总题数分配团队(按工序)分配人员(按工序)进度状态
' + esc(t.name) + '' + esc(t.key) + '' + esc(t.type) + '' + t.total + '' + teamCell(t) + '' + userCell(t) + '' + progressCell(t) + '' + (t.online ? '上线' : '下线') + '
'; } function renderTeamTable() { var agg = {}; // gid -> {name, tasks:[{task, wt}], wtCount:{}} state.tasks.forEach(function (t) { WORK_ORDER.forEach(function (wt) { (t.teamsByWt[wt] || []).forEach(function (g) { var gid = String(g.gid); var name = g.guild_name || state.guildMap[gid] || ('团队' + gid); if (!matchKeyword(name) && !matchKeyword(t.name) && !matchKeyword(t.key)) return; var a = agg[gid] = agg[gid] || { gid: gid, name: name, tasks: [], wtCount: {} }; a.tasks.push({ task: t, wt: wt }); a.wtCount[wt] = (a.wtCount[wt] || 0) + 1; }); }); }); var list = Object.keys(agg).map(function (k) { return agg[k]; }) .sort(function (a, b) { return b.tasks.length - a.tasks.length; }); if (!list.length) return '
没有团队分配记录
'; var h = ''; list.forEach(function (a) { var wtChips = WORK_ORDER.filter(function (wt) { return a.wtCount[wt]; }).map(function (wt) { var c = WORK_COLOR[wt]; return '' + WORK_TYPES[wt] + ' × ' + a.wtCount[wt] + ''; }).join(''); var detail = a.tasks.map(function (x) { var c = WORK_COLOR[x.wt]; return '' + esc(x.task.name) + '' + WORK_TYPES[x.wt] + ''; }).join(' '); h += '' + ''; }); return h + '
团队承接任务数工序分布任务明细(工序)
' + esc(a.name) + '
gid ' + esc(a.gid) + '
' + a.tasks.length + '' + wtChips + '' + detail + '
'; } function renderUserTable() { var agg = {}; state.tasks.forEach(function (t) { WORK_ORDER.forEach(function (wt) { (t.usersByWt[wt] || []).forEach(function (u) { var info = userInfoOf(u); var uid = info.uid || '(无ID)'; if (!matchKeyword(info.account) && !matchKeyword(info.nickname) && !matchKeyword(uid) && !matchKeyword(t.name) && !matchKeyword(t.key)) return; var a = agg[uid] = agg[uid] || { uid: uid, account: info.account, nickname: info.nickname, tasks: [], wtCount: {}, teamBacked: 0 }; if (!a.account && info.account) a.account = info.account; if (!a.nickname && info.nickname) a.nickname = info.nickname; a.tasks.push({ task: t, wt: wt }); a.wtCount[wt] = (a.wtCount[wt] || 0) + 1; if (teamNames(t, wt).length) a.teamBacked++; }); }); }); var list = Object.keys(agg).map(function (k) { return agg[k]; }) .sort(function (a, b) { return b.tasks.length - a.tasks.length; }); if (!list.length) { return '
没有任何按个人分配的记录
(当前任务的分配都在团队维度上)
'; } var h = '' + ''; list.forEach(function (a) { var wts = WORK_ORDER.filter(function (wt) { return a.wtCount[wt]; }).map(function (wt) { var c = WORK_COLOR[wt]; return '' + WORK_TYPES[wt] + ' × ' + a.wtCount[wt] + ''; }).join(''); var detail = a.tasks.map(function (x) { var c = WORK_COLOR[x.wt]; return '' + esc(x.task.name) + '' + '' + WORK_TYPES[x.wt] + ''; }).join(' '); var cover = a.teamBacked === a.tasks.length ? '' + a.tasks.length + '/' + a.tasks.length + ' 有团队' : '' + a.teamBacked + '/' + a.tasks.length + ' 有团队' + '
' + (a.tasks.length - a.teamBacked) + ' 项属个人直派
'; var title = a.account || a.nickname || ('用户' + a.uid); var sub = []; if (a.account) sub.push('账号 ' + a.account); if (a.nickname) sub.push('昵称 ' + a.nickname); sub.push('uid ' + a.uid); h += '' + ''; }); return h + '
人员承接任务数工序分布任务明细(工序)团队覆盖
' + esc(title) + '
' + esc(sub.join(' · ')) + '
' + a.tasks.length + '' + wts + '' + detail + '' + cover + '
'; } function renderUnassigned() { if (!state.tasks.length) return '
暂无数据
'; var none = state.tasks.filter(function (t) { return !hasAnyAssignee(t); }); var userOnly = state.tasks.filter(function (t) { return !hasAnyTeam(t) && hasAnyUser(t); }); var partial = state.tasks.filter(function (t) { return hasAnyAssignee(t) && activeWts(t).some(function (wt) { return wtEmptyOf(t, wt); }); }); var h = ''; h += '
① 完全没人承接(无团队也无个人):' + none.length + ' 个
'; if (none.length) { h += ''; none.forEach(function (t) { var wts = activeWts(t).map(function (wt) { return WORK_TYPES[wt]; }).join(' / '); h += ''; }); h += '
任务名称task_key标注类型总题数启用工序
' + esc(t.name) + '' + esc(t.key) + '' + esc(t.type) + '' + t.total + '' + esc(wts) + '
'; } h += '
② 只按个人分配、没有团队承接:' + userOnly.length + ' 个
'; if (userOnly.length) { h += ''; userOnly.forEach(function (t) { h += ''; }); h += '
任务名称task_key人员(按工序)
' + esc(t.name) + '' + esc(t.key) + '' + userCell(t) + '
'; } h += '
③ 部分工序没人(团队与个人都没有):' + partial.length + ' 个
'; if (partial.length) { h += ''; partial.forEach(function (t) { var missing = activeWts(t).filter(function (wt) { return wtEmptyOf(t, wt); }) .map(function (wt) { return WORK_TYPES[wt]; }).join(' / '); h += '' + '' + ''; }); h += '
任务名称空缺工序团队分配人员分配
' + esc(t.name) + '
' + esc(t.key) + '
' + esc(missing) + '' + teamCell(t) + '' + userCell(t) + '
'; } return h; } function renderRaw() { var sample = state.rawSample; var h = '
用于核对字段。第一条任务的原始 allocation_info / access_info:
'; if (!sample) return h + '
暂无数据
'; var o = {}; ['task_batch_key', 'task_batch_name', 'task_type', 'total_num', 'accept_num', 'check_num', 'inspect_num', 'done_num', 'online_status', 'access_info', 'allocation_info'].forEach(function (k) { o[k] = sample[k]; }); h += '
' + esc(JSON.stringify(o, null, 2)) + '
'; h += '
团队列表接口:
'; h += '
' + esc(JSON.stringify(state.guilds.slice(0, 5), null, 2)) + '
'; return h; } function renderFoot() { var foot = shadow.getElementById('foot'); var fs = cfg.fontScale || 1.15; var fsOpts = [['1', '小'], ['1.15', '标准'], ['1.3', '大'], ['1.5', '特大'], ['1.7', '超大']]; foot.innerHTML = '' + '间隔' + '秒' + '' + '字号' + '' + '只读 · GET'; foot.querySelector('#fs').addEventListener('change', function (e) { cfg.fontScale = Number(e.target.value) || 1.15; saveCfg(); applyGeom(); renderFoot(); }); foot.querySelector('#chkAuto').addEventListener('change', function (e) { cfg.autoRefresh = e.target.checked; saveCfg(); setupTimer(); }); foot.querySelector('#intv').addEventListener('change', function (e) { var v = Math.max(3, Math.min(600, parseInt(e.target.value, 10) || 15)); cfg.intervalSec = v; e.target.value = v; saveCfg(); setupTimer(); }); foot.querySelector('#chkBadge').addEventListener('change', function (e) { cfg.inlineBadge = e.target.checked; saveCfg(); if (cfg.inlineBadge) injectBadges(); else removeBadges(); }); } /* ============================ 页面表格内标注 ============================ */ function removeBadges() { document.querySelectorAll('.dsh-tb-badge').forEach(function (b) { b.remove(); }); } function badgeHtml(task) { var wts = activeWts(task); var teamLine = wts.map(function (wt) { var names = teamNames(task, wt); var color = WORK_COLOR[wt] || '#909399'; var txt = names.length ? names.join('、') : '未分配'; var fg = names.length ? color : '#f56c6c'; return '' + WORK_TYPES[wt] + '·' + esc(txt) + ''; }).join(''); if (!hasAnyUser(task)) return teamLine; var userLine = wts.map(function (wt) { var names = userNames(task, wt); if (!names.length) return ''; var color = WORK_COLOR[wt] || '#909399'; return '' + '👤' + WORK_TYPES[wt] + '·' + esc(names.join('、')) + ''; }).join(''); return teamLine + (userLine ? '
' + userLine + '
' : ''); } function injectBadges() { if (!cfg.inlineBadge) return; var tables = document.querySelectorAll('.el-table__body-wrapper table'); if (!tables.length) return; Array.prototype.forEach.call(tables, function (table) { var ths = table.querySelectorAll('thead th'); if (!ths.length) return; var headers = Array.prototype.map.call(ths, function (th) { return (th.innerText || '').trim(); }); var keyIdx = headers.indexOf('任务key'); if (keyIdx < 1) { // 兜底:找哪一列的内容像 task_key keyIdx = headers.length > 1 ? 1 : -1; } if (keyIdx < 1) return; var trs = table.querySelectorAll('tbody tr'); Array.prototype.forEach.call(trs, function (tr) { var tds = tr.querySelectorAll('td'); if (tds.length <= keyIdx) return; var taskKey = (tds[keyIdx].innerText || '').trim(); var task = state.taskMap[taskKey]; var nameCell = tds[keyIdx - 1]; if (!nameCell) return; var cell = nameCell.querySelector('.cell') || nameCell; if (!task) { var old = cell.querySelector('.dsh-tb-badge'); if (old) old.remove(); return; } var badge = cell.querySelector('.dsh-tb-badge'); if (!badge) { badge = document.createElement('div'); badge.className = 'dsh-tb-badge'; badge.style.cssText = 'margin-top:3px;display:flex;flex-wrap:wrap;gap:3px;'; cell.appendChild(badge); } var html = badgeHtml(task); if (badge.getAttribute('data-sig') !== html) { badge.setAttribute('data-sig', html); badge.innerHTML = html; } }); }); } function ensureBadgeStyles() { if (document.getElementById('dsh-tb-badge-style')) return; var s = document.createElement('style'); s.id = 'dsh-tb-badge-style'; s.textContent = '.dsh-tb-chip{display:inline-block;padding:1px 7px;border-radius:9px;' + 'font-size:12.5px;line-height:19px;border:1px solid transparent;white-space:nowrap;}'; document.head.appendChild(s); } /* ============================ 页面 XHR 钩子(同步兜底) ============================ */ function hookXhr() { try { var XP = XMLHttpRequest.prototype; var rawOpen = XP.open, rawSend = XP.send; XP.open = function (method, url) { try { this.__dsh_url = url; this.__dsh_method = method; } catch (e) {} return rawOpen.apply(this, arguments); }; XP.send = function () { var xhr = this; try { xhr.addEventListener('load', function () { try { var u = xhr.__dsh_url || ''; if (!/\/task-batches(\?|$)/.test(u)) return; if (String(xhr.__dsh_method).toUpperCase() !== 'GET') return; var res = JSON.parse(xhr.responseText); if (!isOk(res)) return; var items = (res.data && res.data.items) || []; if (!items.length) return; // 页面自己拉到的数据:仅在接口拉取失败时作为兜底 if (state.error) { state.tasks = items.map(normalizeTask); state.taskMap = {}; state.tasks.forEach(function (t) { if (t.key) state.taskMap[t.key] = t; }); state.rawSample = items[0]; state.source = 'page'; state.lastUpdate = new Date(); state.error = ''; render(); } injectBadges(); } catch (e) {} }); } catch (e) {} return rawSend.apply(this, arguments); }; } catch (e) {} } /* ============================ 定时 / 观察 ============================ */ function setupTimer() { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } if (!cfg.autoRefresh) return; refreshTimer = setInterval(function () { if (document.hidden) return; loadAll(); }, Math.max(3, cfg.intervalSec) * 1000); } function setupObserver() { if (domObserver) domObserver.disconnect(); domObserver = new MutationObserver(function () { if (badgeTimer) return; badgeTimer = setTimeout(function () { badgeTimer = null; try { injectBadges(); } catch (e) {} }, 300); }); domObserver.observe(document.body, { childList: true, subtree: true }); } function setupRouteWatch() { setInterval(function () { if (location.pathname === currentPath) return; currentPath = location.pathname; if (/\/v\/project\/[^/]+\/project-taskManage/.test(currentPath)) { ensurePanel(); if (host) host.style.display = ''; barSig = null; loadAll(); } else if (host) { host.style.display = 'none'; } }, 800); window.addEventListener('resize', function () { applyGeom(); }); } /* ============================ 导出 ============================ */ function exportCsv() { var lines = []; lines.push(['任务名称', 'task_key', '标注类型', '总题数', '工序', '分配团队', '分配人员', '已完成', '启用'].join(',')); state.tasks.forEach(function (t) { activeWts(t).forEach(function (wt) { var names = teamNames(t, wt); lines.push([ csvCell(t.name), csvCell(t.key), csvCell(t.type), t.total, WORK_TYPES[wt], csvCell(names.join('、') || '未分配'), csvCell(userNames(t, wt).join('、')), (t.progress[wt] || 0) + '/' + t.total, wtEnabled(t, wt) ? '是' : '否' ].join(',')); }); }); var blob = new Blob(['\ufeff' + lines.join('\r\n')], { type: 'text/csv;charset=utf-8' }); var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = '任务团队分配_' + projectKey() + '_' + Date.now() + '.csv'; document.body.appendChild(a); a.click(); setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000); } function csvCell(v) { var s = String(v === undefined || v === null ? '' : v); if (/[",\r\n]/.test(s)) s = '"' + s.replace(/"/g, '""') + '"'; return s; } /* ============================ 启动 ============================ */ function boot() { if (!/\/v\/project\/[^/]+\/project-taskManage/.test(location.pathname)) return; ensureBadgeStyles(); ensurePanel(); hookXhr(); setupObserver(); setupRouteWatch(); setupTimer(); loadAll(); } if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(boot, 300); } else { window.addEventListener('DOMContentLoaded', function () { setTimeout(boot, 300); }); } })();