Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4da7d4e94a | |||
|
|
b849b08386 | ||
|
|
660fb811a2 | ||
|
|
f549059e47 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -79,6 +79,7 @@ web_modules/
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.local
|
.env.local
|
||||||
|
env.js
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
.cache
|
.cache
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
# Teams 别名插件
|
# Teams 别名插件
|
||||||
|
|
||||||
给 Microsoft Teams 好友的名称添加别名。
|
给 Microsoft Teams 好友的名称添加别名。
|
||||||
|
|
||||||
|
在插件弹窗中导入 CSV 文件作为数据源。CSV 需要包含“账号ID”和“最终输出”两列,插件会用“账号ID”匹配 Teams 用户,并显示对应的“最终输出”。
|
||||||
|
|||||||
50
content.js
50
content.js
@@ -9,19 +9,6 @@ const PEOPLE_PICKER_SEL_PREFIX = "people-picker-selected-user-";
|
|||||||
let debounceTimer = null;
|
let debounceTimer = null;
|
||||||
let isMutating = false;
|
let isMutating = false;
|
||||||
|
|
||||||
async function saveAlias(id, alias) {
|
|
||||||
const key = id.replace(PERSON_ID_PREFIX, "");
|
|
||||||
const result = await chrome.storage.local.get('aliases');
|
|
||||||
const aliases = result.aliases || {};
|
|
||||||
// 新的别名为空就删除
|
|
||||||
if (alias) {
|
|
||||||
aliases[key] = alias;
|
|
||||||
} else {
|
|
||||||
delete aliases[key];
|
|
||||||
}
|
|
||||||
await chrome.storage.local.set({ aliases });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAlias(id) {
|
async function getAlias(id) {
|
||||||
const key = id.replace(PERSON_ID_PREFIX, "");
|
const key = id.replace(PERSON_ID_PREFIX, "");
|
||||||
const result = await chrome.storage.local.get('aliases');
|
const result = await chrome.storage.local.get('aliases');
|
||||||
@@ -34,44 +21,37 @@ function applyAliasAndButton(el) {
|
|||||||
const id = el.id;
|
const id = el.id;
|
||||||
if (!id || !id.startsWith(PERSON_ID_PREFIX)) return;
|
if (!id || !id.startsWith(PERSON_ID_PREFIX)) return;
|
||||||
|
|
||||||
|
const rawId = id.replace(PERSON_ID_PREFIX, "");
|
||||||
|
|
||||||
const existingBtn = document.querySelector(`[data-floating-btn-for="${id}"]`);
|
const existingBtn = document.querySelector(`[data-floating-btn-for="${id}"]`);
|
||||||
if (!existingBtn) {
|
if (!existingBtn) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.textContent = '设置别名';
|
button.textContent = '显示ID'; // 改为显示ID
|
||||||
button.style.position = 'fixed';
|
button.style.position = 'fixed';
|
||||||
button.style.left = `${rect.left + window.scrollX}px`;
|
button.style.left = `${rect.left + window.scrollX}px`;
|
||||||
button.style.top = `${rect.bottom + window.scrollY + 20}px`;
|
button.style.top = `${rect.bottom + window.scrollY + 20}px`;
|
||||||
button.style.zIndex = '99999';
|
button.style.zIndex = '99999';
|
||||||
button.style.padding = '4px 8px';
|
button.style.padding = '4px 8px';
|
||||||
button.style.fontSize = '12px';
|
button.style.fontSize = '12px';
|
||||||
button.style.backgroundColor = '#0078d4';
|
button.style.backgroundColor = '#6c757d'; // 灰色,表示只是信息查看
|
||||||
button.style.color = '#fff';
|
button.style.color = '#fff';
|
||||||
button.style.border = 'none';
|
button.style.border = 'none';
|
||||||
button.style.borderRadius = '4px';
|
button.style.borderRadius = '4px';
|
||||||
button.style.cursor = 'pointer';
|
button.style.cursor = 'pointer';
|
||||||
button.setAttribute('data-floating-btn-for', id);
|
button.setAttribute('data-floating-btn-for', id);
|
||||||
|
|
||||||
button.addEventListener('click', async () => {
|
button.addEventListener('click', () => {
|
||||||
const current = (await getAlias(id.replace(PERSON_ID_PREFIX, ""))) || document.getElementById(id)?.textContent || '';
|
// 弹窗显示 ID,方便复制
|
||||||
const newAlias = prompt("请输入别名:", current);
|
prompt("该用户的账号ID为 (请复制):", rawId);
|
||||||
// 如果是空字符串还是得进入
|
|
||||||
if (newAlias !== null) {
|
|
||||||
const el = document.getElementById(id); // 🔥 重新获取最新的元素
|
|
||||||
if (el && newAlias) el.textContent = newAlias.trim();
|
|
||||||
await saveAlias(id.replace(PERSON_ID_PREFIX, ""), newAlias.trim());
|
|
||||||
}
|
|
||||||
if (newAlias === "") {
|
|
||||||
alert("别名已删除");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.appendChild(button);
|
document.body.appendChild(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用别名(异步)
|
// 应用别名(异步)
|
||||||
getAlias(id.replace(PERSON_ID_PREFIX, "")).then(alias => {
|
getAlias(rawId).then(alias => {
|
||||||
if (alias && el.textContent !== alias) {
|
if (alias && el.textContent !== alias) {
|
||||||
el.textContent = alias;
|
el.textContent = alias;
|
||||||
}
|
}
|
||||||
@@ -122,12 +102,12 @@ function applyLeftChatAlias(el) {
|
|||||||
if (!id || !id.startsWith(ICON_ID_PREFIX)) return;
|
if (!id || !id.startsWith(ICON_ID_PREFIX)) return;
|
||||||
|
|
||||||
let parent = el;
|
let parent = el;
|
||||||
// 向上查找 3 个父元素
|
// 向上查找 4 个父元素
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
if (parent.parentElement) {
|
if (parent.parentElement) {
|
||||||
parent = parent.parentElement;
|
parent = parent.parentElement;
|
||||||
} else {
|
} else {
|
||||||
return; // 如果不足3层,就跳过
|
return; // 如果不足4层,就跳过
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,17 +115,17 @@ function applyLeftChatAlias(el) {
|
|||||||
const nextSibling = parent.nextElementSibling;
|
const nextSibling = parent.nextElementSibling;
|
||||||
if (!nextSibling) return;
|
if (!nextSibling) return;
|
||||||
|
|
||||||
// 向下查找第 2 个子元素(层级式)
|
// 向下查找第 7 个子元素(层级式)
|
||||||
let target = nextSibling;
|
let target = nextSibling;
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
if (target.children.length > 0) {
|
if (target.children.length > 0) {
|
||||||
target = target.children[0]; // 每层往下取第一个子元素
|
target = target.children[0]; // 每层往下取第一个子元素
|
||||||
} else {
|
} else {
|
||||||
return; // 不足2层,跳过
|
return; // 不足7层,跳过
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 判断符合才修改
|
// 判断符合才修改
|
||||||
if (target.getAttribute('data-tid') === 'chat-list-item-title') {
|
if (target.id.startsWith('title-chat-list-item')) {
|
||||||
getAlias(id.replace(ICON_ID_PREFIX, "")).then(alias => {
|
getAlias(id.replace(ICON_ID_PREFIX, "")).then(alias => {
|
||||||
if (alias && target.textContent !== alias) {
|
if (alias && target.textContent !== alias) {
|
||||||
target.textContent = alias;
|
target.textContent = alias;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Teams 别名管理",
|
"name": "Teams 别名管理",
|
||||||
"version": "0.1.0",
|
"version": "0.4.0",
|
||||||
"icons": {
|
"icons": {
|
||||||
"16": "icons/teams-alias-16.png",
|
"16": "icons/teams-alias-16.png",
|
||||||
"32": "icons/teams-alias-32.png",
|
"32": "icons/teams-alias-32.png",
|
||||||
@@ -9,8 +9,10 @@
|
|||||||
"128": "icons/teams-alias-128.png"
|
"128": "icons/teams-alias-128.png"
|
||||||
},
|
},
|
||||||
"description": "给 Teams 好友设置别名",
|
"description": "给 Teams 好友设置别名",
|
||||||
"permissions": ["storage", "scripting"],
|
"permissions": ["storage"],
|
||||||
"host_permissions": ["https://teams.live.com/v2*"],
|
"host_permissions": [
|
||||||
|
"https://teams.live.com/v2*"
|
||||||
|
],
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["https://teams.live.com/v2*"],
|
"matches": ["https://teams.live.com/v2*"],
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "teams-alias",
|
"name": "teams-alias",
|
||||||
"version": "0.1.0",
|
"version": "0.4.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "teams-alias",
|
"name": "teams-alias",
|
||||||
"version": "0.1.0",
|
"version": "0.4.0",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chrome-types": "^0.1.347"
|
"chrome-types": "^0.1.347"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "teams-alias",
|
"name": "teams-alias",
|
||||||
"version": "0.1.0",
|
"version": "0.4.0",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
|||||||
18
popup.html
18
popup.html
@@ -8,7 +8,7 @@
|
|||||||
background-color: #f9fafb;
|
background-color: #f9fafb;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
width: 120px;
|
width: 160px;
|
||||||
color: #111827;
|
color: #111827;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,15 +50,25 @@
|
|||||||
#fileInput {
|
#fileInput {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#status {
|
||||||
|
margin-top: 8px;
|
||||||
|
min-height: 15px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #4b5563;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<title>Teams 别名管理</title>
|
<title>Teams 别名管理</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h3>Teams 别名管理</h3>
|
<h3>Teams 别名管理</h3>
|
||||||
|
<input id="fileInput" type="file" accept=".csv,text/csv">
|
||||||
|
<button id="import">导入 CSV</button>
|
||||||
<button id="export">导出</button>
|
<button id="export">导出</button>
|
||||||
<button id="import">覆盖导入</button>
|
<div id="status"></div>
|
||||||
<button id="update-import">合并导入</button>
|
|
||||||
<input type="file" id="fileInput" accept="application/json" />
|
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
180
popup.js
180
popup.js
@@ -1,3 +1,137 @@
|
|||||||
|
const REQUIRED_ID_FIELD = '账号ID';
|
||||||
|
const REQUIRED_ALIAS_FIELD = '最终输出';
|
||||||
|
|
||||||
|
function setStatus(message) {
|
||||||
|
document.getElementById('status').textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCsv(text) {
|
||||||
|
const rows = [];
|
||||||
|
let row = [];
|
||||||
|
let cell = '';
|
||||||
|
let inQuotes = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const char = text[i];
|
||||||
|
const nextChar = text[i + 1];
|
||||||
|
|
||||||
|
if (char === '"') {
|
||||||
|
if (inQuotes && nextChar === '"') {
|
||||||
|
cell += '"';
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
inQuotes = !inQuotes;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === ',' && !inQuotes) {
|
||||||
|
row.push(cell);
|
||||||
|
cell = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((char === '\n' || char === '\r') && !inQuotes) {
|
||||||
|
if (char === '\r' && nextChar === '\n') i++;
|
||||||
|
row.push(cell);
|
||||||
|
if (row.some(value => value.trim() !== '')) rows.push(row);
|
||||||
|
row = [];
|
||||||
|
cell = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cell += char;
|
||||||
|
}
|
||||||
|
|
||||||
|
row.push(cell);
|
||||||
|
if (row.some(value => value.trim() !== '')) rows.push(row);
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeHeader(value) {
|
||||||
|
return value.replace(/^\uFEFF/, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function aliasesFromCsv(text) {
|
||||||
|
const rows = parseCsv(text);
|
||||||
|
if (rows.length === 0) {
|
||||||
|
throw new Error('CSV 文件为空。');
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = rows[0].map(normalizeHeader);
|
||||||
|
const idIndex = headers.indexOf(REQUIRED_ID_FIELD);
|
||||||
|
const aliasIndex = headers.indexOf(REQUIRED_ALIAS_FIELD);
|
||||||
|
|
||||||
|
if (idIndex === -1 || aliasIndex === -1) {
|
||||||
|
throw new Error(`CSV 必须包含“${REQUIRED_ID_FIELD}”和“${REQUIRED_ALIAS_FIELD}”两列。`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliases = {};
|
||||||
|
|
||||||
|
rows.slice(1).forEach(row => {
|
||||||
|
const id = (row[idIndex] || '').trim();
|
||||||
|
const alias = (row[aliasIndex] || '').trim();
|
||||||
|
|
||||||
|
if (id && alias) {
|
||||||
|
aliases[id] = alias;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return aliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeCsvBuffer(buffer, encoding) {
|
||||||
|
return new TextDecoder(encoding).decode(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readAliasesFromCsvBuffer(buffer) {
|
||||||
|
const encodings = ['utf-8', 'gb18030', 'gbk'];
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
for (const encoding of encodings) {
|
||||||
|
try {
|
||||||
|
return aliasesFromCsv(decodeCsvBuffer(buffer, encoding));
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入 CSV
|
||||||
|
document.getElementById('import').addEventListener('click', () => {
|
||||||
|
document.getElementById('fileInput').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('fileInput').addEventListener('change', async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const btn = document.getElementById('import');
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = '导入中...';
|
||||||
|
btn.disabled = true;
|
||||||
|
setStatus('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
const aliases = readAliasesFromCsvBuffer(buffer);
|
||||||
|
await chrome.storage.local.set({ aliases, lastImport: Date.now(), sourceFileName: file.name });
|
||||||
|
setStatus(`已导入 ${Object.keys(aliases).length} 条`);
|
||||||
|
alert(`导入成功!共导入 ${Object.keys(aliases).length} 条数据。`);
|
||||||
|
} catch (error) {
|
||||||
|
setStatus('导入失败');
|
||||||
|
alert(error.message || '导入失败,请检查 CSV 文件。');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
btn.textContent = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 导出数据
|
// 导出数据
|
||||||
document.getElementById('export').addEventListener('click', async () => {
|
document.getElementById('export').addEventListener('click', async () => {
|
||||||
const { aliases } = await chrome.storage.local.get('aliases');
|
const { aliases } = await chrome.storage.local.get('aliases');
|
||||||
@@ -16,49 +150,3 @@ document.getElementById('export').addEventListener('click', async () => {
|
|||||||
|
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 通用导入处理函数
|
|
||||||
function handleFileImport(mode = 'overwrite') {
|
|
||||||
const fileInput = document.getElementById('fileInput');
|
|
||||||
|
|
||||||
const handler = async (e) => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async function (event) {
|
|
||||||
try {
|
|
||||||
const importedData = JSON.parse(event.target.result);
|
|
||||||
if (typeof importedData !== 'object' || importedData === null) {
|
|
||||||
alert('文件格式错误');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { aliases: existingAliases = {} } = await chrome.storage.local.get('aliases');
|
|
||||||
|
|
||||||
const updatedAliases = (mode === 'merge')
|
|
||||||
? { ...importedData, ...existingAliases } // 保留现有的不被导入的覆盖
|
|
||||||
: importedData;
|
|
||||||
|
|
||||||
await chrome.storage.local.set({ aliases: updatedAliases });
|
|
||||||
|
|
||||||
alert(mode === 'merge' ? '导入并合并成功' : '导入成功');
|
|
||||||
} catch (err) {
|
|
||||||
alert('无法解析 JSON 文件');
|
|
||||||
} finally {
|
|
||||||
fileInput.removeEventListener('change', handler);
|
|
||||||
fileInput.value = ''; // 重置 input 以便后续重复导入同一文件
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', handler, { once: true });
|
|
||||||
fileInput.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导入(覆盖)
|
|
||||||
document.getElementById('import').addEventListener('click', () => handleFileImport('overwrite'));
|
|
||||||
|
|
||||||
// 更新导入(合并)
|
|
||||||
document.getElementById('update-import').addEventListener('click', () => handleFileImport('merge'));
|
|
||||||
|
|||||||
Reference in New Issue
Block a user