add all files
This commit is contained in:
@@ -1,3 +1,3 @@
|
|||||||
# teams-alias
|
# Teams 别名插件
|
||||||
|
|
||||||
Teams 别名插件
|
给 Microsoft Teams 好友的名称添加别名。
|
||||||
|
|||||||
428
content.js
Normal file
428
content.js
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
const PERSON_ID_PREFIX = 'chat-topic-person-';
|
||||||
|
const ICON_ID_PREFIX = "presence-pill-";
|
||||||
|
const CHAT_ROSTER_PREFIX = "chat-roster-item-name-";
|
||||||
|
const SUGGEST_PEOPLE_PREFIX = "AUTOSUGGEST_SUGGESTION_PEOPLE";
|
||||||
|
const ROSTER_AVATAR_PREFIX = "roster-avatar-img-";
|
||||||
|
const SERP_PEOPLE_CARD_PREFIX = "serp-people-card-content-";
|
||||||
|
const PEOPLE_PICKER_PREFIX = "people-picker-entry-";
|
||||||
|
const PEOPLE_PICKER_SEL_PREFIX = "people-picker-selected-user-";
|
||||||
|
let debounceTimer = null;
|
||||||
|
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) {
|
||||||
|
const key = id.replace(PERSON_ID_PREFIX, "");
|
||||||
|
const result = await chrome.storage.local.get('aliases');
|
||||||
|
const aliases = result.aliases || {};
|
||||||
|
return aliases[key] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置别名显示 + 按钮添加
|
||||||
|
function applyAliasAndButton(el) {
|
||||||
|
const id = el.id;
|
||||||
|
if (!id || !id.startsWith(PERSON_ID_PREFIX)) return;
|
||||||
|
|
||||||
|
const existingBtn = document.querySelector(`[data-floating-btn-for="${id}"]`);
|
||||||
|
if (!existingBtn) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.textContent = '设置别名';
|
||||||
|
button.style.position = 'fixed';
|
||||||
|
button.style.left = `${rect.left + window.scrollX}px`;
|
||||||
|
button.style.top = `${rect.bottom + window.scrollY + 20}px`;
|
||||||
|
button.style.zIndex = '99999';
|
||||||
|
button.style.padding = '4px 8px';
|
||||||
|
button.style.fontSize = '12px';
|
||||||
|
button.style.backgroundColor = '#0078d4';
|
||||||
|
button.style.color = '#fff';
|
||||||
|
button.style.border = 'none';
|
||||||
|
button.style.borderRadius = '4px';
|
||||||
|
button.style.cursor = 'pointer';
|
||||||
|
button.setAttribute('data-floating-btn-for', id);
|
||||||
|
|
||||||
|
button.addEventListener('click', async () => {
|
||||||
|
const current = (await getAlias(id.replace(PERSON_ID_PREFIX, ""))) || document.getElementById(id)?.textContent || '';
|
||||||
|
const newAlias = prompt("请输入别名:", current);
|
||||||
|
// 如果是空字符串还是得进入
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用别名(异步)
|
||||||
|
getAlias(id.replace(PERSON_ID_PREFIX, "")).then(alias => {
|
||||||
|
if (alias && el.textContent !== alias) {
|
||||||
|
el.textContent = alias;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主要查找右侧消息列表中的名字并修改
|
||||||
|
function applyRightChatAlias(el) {
|
||||||
|
let id = el.id;
|
||||||
|
if (!id || !id.startsWith(ICON_ID_PREFIX)) return;
|
||||||
|
|
||||||
|
let parent = el;
|
||||||
|
// 向上查找 4 个父元素
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (parent.parentElement) {
|
||||||
|
parent = parent.parentElement;
|
||||||
|
} else {
|
||||||
|
return; // 如果不足4层,就跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取前一个兄弟元素
|
||||||
|
const prevSibling = parent.previousElementSibling;
|
||||||
|
if (!prevSibling) return;
|
||||||
|
|
||||||
|
// 向下查找第 4 个子元素(层级式)
|
||||||
|
let target = prevSibling;
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (target.children.length > 0) {
|
||||||
|
target = target.children[0]; // 每层往下取第一个子元素
|
||||||
|
} else {
|
||||||
|
return; // 不足4层,跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 判断符合才修改
|
||||||
|
if (target.getAttribute('data-tid') === 'message-author-name') {
|
||||||
|
getAlias(id.replace(ICON_ID_PREFIX, "")).then(alias => {
|
||||||
|
if (alias && target.textContent !== alias) {
|
||||||
|
target.textContent = alias;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主要查找左侧消息列表中的名字并修改
|
||||||
|
function applyLeftChatAlias(el) {
|
||||||
|
let id = el.id;
|
||||||
|
if (!id || !id.startsWith(ICON_ID_PREFIX)) return;
|
||||||
|
|
||||||
|
let parent = el;
|
||||||
|
// 向上查找 3 个父元素
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (parent.parentElement) {
|
||||||
|
parent = parent.parentElement;
|
||||||
|
} else {
|
||||||
|
return; // 如果不足3层,就跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取后一个兄弟元素
|
||||||
|
const nextSibling = parent.nextElementSibling;
|
||||||
|
if (!nextSibling) return;
|
||||||
|
|
||||||
|
// 向下查找第 2 个子元素(层级式)
|
||||||
|
let target = nextSibling;
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
if (target.children.length > 0) {
|
||||||
|
target = target.children[0]; // 每层往下取第一个子元素
|
||||||
|
} else {
|
||||||
|
return; // 不足2层,跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 判断符合才修改
|
||||||
|
if (target.getAttribute('data-tid') === 'chat-list-item-title') {
|
||||||
|
getAlias(id.replace(ICON_ID_PREFIX, "")).then(alias => {
|
||||||
|
if (alias && target.textContent !== alias) {
|
||||||
|
target.textContent = alias;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改群组人员的名称
|
||||||
|
function applyChatRosterAlias(el) {
|
||||||
|
let id = el.id;
|
||||||
|
if (!id || !id.startsWith(CHAT_ROSTER_PREFIX)) return;
|
||||||
|
|
||||||
|
getAlias(id.replace(CHAT_ROSTER_PREFIX, "")).then(alias => {
|
||||||
|
if (alias && el.textContent !== alias) {
|
||||||
|
el.textContent = alias;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 群组添加人员的别名
|
||||||
|
function applyPeoplePickerAlias(el) {
|
||||||
|
let tid = el.getAttribute('data-tid');
|
||||||
|
if (!tid || !tid.startsWith(PEOPLE_PICKER_PREFIX)) return;
|
||||||
|
|
||||||
|
let child = el.children[1];
|
||||||
|
// 向下查找第 3 个子元素(层级式)
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (child.children.length > 0) {
|
||||||
|
child = child.children[0]; // 每层往下取第一个子元素
|
||||||
|
} else {
|
||||||
|
return; // 不足3层,跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (child.tagName.toLowerCase() === 'span') {
|
||||||
|
let id = "8:" + tid.replace(PEOPLE_PICKER_PREFIX, "");
|
||||||
|
getAlias(id).then(alias => {
|
||||||
|
if (alias && child.textContent !== alias) {
|
||||||
|
child.textContent = alias;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 群组添加人员时选中的人员
|
||||||
|
function applyPeoplePickerSelectedAlias(el) {
|
||||||
|
let tid = el.getAttribute('data-tid');
|
||||||
|
if (!tid || !tid.startsWith(PEOPLE_PICKER_SEL_PREFIX)) return;
|
||||||
|
|
||||||
|
let id = "8:" + tid.replace(PEOPLE_PICKER_SEL_PREFIX, "");
|
||||||
|
getAlias(id).then(alias => {
|
||||||
|
if (alias && el.textContent !== alias) {
|
||||||
|
el.textContent = alias;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加搜索框中的人员别名
|
||||||
|
function applySuggestPeopleAlias(el) {
|
||||||
|
let tid = el.getAttribute('data-tid');
|
||||||
|
if (!tid || !tid.startsWith(SUGGEST_PEOPLE_PREFIX)) return;
|
||||||
|
|
||||||
|
let child = el.children[1]; // 第二个子元素
|
||||||
|
// 向下查找第 2 个子元素(层级式)
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
if (child.children.length > 0) {
|
||||||
|
child = child.children[0]; // 每层往下取第一个子元素
|
||||||
|
} else {
|
||||||
|
return; // 不足2层,跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (child.getAttribute('data-tid') !== 'AUTOSUGGEST_SUGGESTION_TITLE') return;
|
||||||
|
|
||||||
|
let id = tid.replace(SUGGEST_PEOPLE_PREFIX, "");
|
||||||
|
getAlias(id).then(alias => {
|
||||||
|
if (alias) {
|
||||||
|
let lastSpan = child.lastElementChild;
|
||||||
|
if (lastSpan.id.startsWith("suggest-alias-attached")) {
|
||||||
|
if (lastSpan.textContent === `[${alias}]`) return;
|
||||||
|
lastSpan.textContent = `[${alias}]`;
|
||||||
|
} else {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.id = `suggest-alias-attached-${id}`;
|
||||||
|
span.textContent = `[${alias}]`;
|
||||||
|
span.style.marginLeft = '4px';
|
||||||
|
span.style.color = document.documentElement.classList.contains("theme-tfl-default") ? '#ed0833' : '#78ef0b';
|
||||||
|
child.appendChild(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加人员搜索中的别名
|
||||||
|
function applySerpPeopleAlias(el) {
|
||||||
|
let id = el.id;
|
||||||
|
if (!id || !id.startsWith(SERP_PEOPLE_CARD_PREFIX)) return;
|
||||||
|
|
||||||
|
let child = el.children[2];
|
||||||
|
// 向下查找第 4 个子元素(层级式)
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (child.children.length > 0) {
|
||||||
|
child = child.children[0]; // 每层往下取第一个子元素
|
||||||
|
} else {
|
||||||
|
return; // 不足4层,跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id = id.replace(SERP_PEOPLE_CARD_PREFIX, "");
|
||||||
|
getAlias(id).then(alias => {
|
||||||
|
if (alias) {
|
||||||
|
let lastSpan = child.lastElementChild;
|
||||||
|
if (lastSpan.id.startsWith("people-card-attached")) {
|
||||||
|
if (lastSpan.textContent === `[${alias}]`) return;
|
||||||
|
lastSpan.textContent = `[${alias}]`;
|
||||||
|
} else {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.id = `people-card-attached-${id}`;
|
||||||
|
span.textContent = `[${alias}]`;
|
||||||
|
span.style.marginLeft = '4px';
|
||||||
|
span.style.color = document.documentElement.classList.contains("theme-tfl-default") ? '#ed0833' : '#78ef0b';
|
||||||
|
child.appendChild(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改通话中的人名
|
||||||
|
function applyCallingAlias(el) {
|
||||||
|
if (el.getAttribute('data-cid') !== 'calling-participant-stream') return;
|
||||||
|
|
||||||
|
let child = el.children[1];
|
||||||
|
// 向下查找第 5 个子元素(层级式)
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
if (child.children.length > 0) {
|
||||||
|
child = child.children[0]; // 每层往下取第一个子元素
|
||||||
|
} else {
|
||||||
|
return; // 不足5层,跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (child.tagName.toLowerCase() === 'span') {
|
||||||
|
getAlias(el.getAttribute('data-acc-element-id')).then(alias => {
|
||||||
|
if (alias && child.textContent !== alias) {
|
||||||
|
child.textContent = alias;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改通话右侧人名的别名
|
||||||
|
function applyRosterAvatarAlias(el) {
|
||||||
|
let id = el.id;
|
||||||
|
if (!id || !id.startsWith(ROSTER_AVATAR_PREFIX)) return;
|
||||||
|
|
||||||
|
getAlias(id.replace(ROSTER_AVATAR_PREFIX, "")).then(alias => {
|
||||||
|
if (alias && el.textContent !== alias) {
|
||||||
|
el.textContent = alias;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看此通话中的人员
|
||||||
|
function applyPeopleInCall(el) {
|
||||||
|
let id = el.id;
|
||||||
|
if (!id || !id.startsWith(ICON_ID_PREFIX)) return;
|
||||||
|
|
||||||
|
let parent = el;
|
||||||
|
// 向上查找 2 个父元素
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
if (parent.parentElement) {
|
||||||
|
parent = parent.parentElement;
|
||||||
|
} else {
|
||||||
|
return; // 如果不足2层,就跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 一个是查看此通话中的参与者,一个是呼叫其他人加入
|
||||||
|
if ([
|
||||||
|
"audio_dropin_add_participants_dialog_renderer",
|
||||||
|
"audio-drop-in-live-roster"
|
||||||
|
].includes(parent.getAttribute("data-tid"))) {
|
||||||
|
let target = parent.nextElementSibling;
|
||||||
|
if (!target) return;
|
||||||
|
if (target.tagName.toLowerCase() === "span") {
|
||||||
|
getAlias(id.replace(ICON_ID_PREFIX, "")).then(alias => {
|
||||||
|
if (alias && target.textContent !== alias) {
|
||||||
|
target.textContent = alias;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回应表情的人员别名
|
||||||
|
function applyReactionAlias(el) {
|
||||||
|
if (el.getAttribute('data-tid') !== 'diverse-reaction-user-list-item') return;
|
||||||
|
try {
|
||||||
|
const tabster = JSON.parse(el.getAttribute("data-tabster"));
|
||||||
|
|
||||||
|
let child = el.children[1];
|
||||||
|
let target = child.children[0];
|
||||||
|
let id = tabster.observed.names[0];
|
||||||
|
getAlias(id).then(alias => {
|
||||||
|
if (alias && target.textContent !== alias) {
|
||||||
|
target.textContent = alias;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找所有目标元素应用别名和按钮
|
||||||
|
function applyToAll() {
|
||||||
|
document.querySelectorAll('[data-floating-btn-for]').forEach(btn => btn.remove());
|
||||||
|
|
||||||
|
const allPersons = document.querySelectorAll(`[id^="${PERSON_ID_PREFIX}"]`);
|
||||||
|
allPersons.forEach(el => applyAliasAndButton(el));
|
||||||
|
|
||||||
|
const allIcons = document.querySelectorAll(`[id^="${ICON_ID_PREFIX}"]`);
|
||||||
|
allIcons.forEach(el => {
|
||||||
|
applyRightChatAlias(el);
|
||||||
|
applyLeftChatAlias(el);
|
||||||
|
applyPeopleInCall(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
const allChatRoster = document.querySelectorAll(`[id^="${CHAT_ROSTER_PREFIX}"]`);
|
||||||
|
allChatRoster.forEach(el => applyChatRosterAlias(el));
|
||||||
|
|
||||||
|
const allSuggestPeople = document.querySelectorAll(`[data-tid^="${SUGGEST_PEOPLE_PREFIX}"]`);
|
||||||
|
allSuggestPeople.forEach(el => applySuggestPeopleAlias(el));
|
||||||
|
|
||||||
|
const allCalling = document.querySelectorAll(`[data-cid="calling-participant-stream"]`);
|
||||||
|
allCalling.forEach(el => applyCallingAlias(el));
|
||||||
|
|
||||||
|
const allRosterAvatar = document.querySelectorAll(`[id^="${ROSTER_AVATAR_PREFIX}"]`);
|
||||||
|
allRosterAvatar.forEach(el => applyRosterAvatarAlias(el));
|
||||||
|
|
||||||
|
const allSerpPeople = document.querySelectorAll(`[id^="${SERP_PEOPLE_CARD_PREFIX}"]`);
|
||||||
|
allSerpPeople.forEach(el => applySerpPeopleAlias(el));
|
||||||
|
|
||||||
|
const allPeoplePicker = document.querySelectorAll(`[data-tid^="${PEOPLE_PICKER_PREFIX}"]`);
|
||||||
|
allPeoplePicker.forEach(el => applyPeoplePickerAlias(el));
|
||||||
|
|
||||||
|
const allPeoplePickerSelected = document.querySelectorAll(`[data-tid^="${PEOPLE_PICKER_SEL_PREFIX}"]`);
|
||||||
|
allPeoplePickerSelected.forEach(el => applyPeoplePickerSelectedAlias(el));
|
||||||
|
|
||||||
|
const allReaction = document.querySelectorAll(`[data-tid="diverse-reaction-user-list-item"]`);
|
||||||
|
allReaction.forEach(el => applyReactionAlias(el));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化逻辑
|
||||||
|
function init() {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
if (isMutating) return; // 🧠 防止自己触发自己
|
||||||
|
isMutating = true;
|
||||||
|
|
||||||
|
applyToAll(); // 页面内容变动后重新应用
|
||||||
|
|
||||||
|
// 给浏览器一点时间完成 DOM 更新后再允许 observer 响应
|
||||||
|
setTimeout(() => {
|
||||||
|
isMutating = false;
|
||||||
|
}, 500); // 至少比这个高才行,不然会一直触发
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
applyToAll(); // 初始执行
|
||||||
|
|
||||||
|
// 兜底:每 2 秒再扫一次(避免漏掉异步更新)
|
||||||
|
// setInterval(() => {
|
||||||
|
// applyToAll();
|
||||||
|
// }, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
BIN
icons/teams-alias-128.png
Normal file
BIN
icons/teams-alias-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
icons/teams-alias-16.png
Normal file
BIN
icons/teams-alias-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 966 B |
BIN
icons/teams-alias-32.png
Normal file
BIN
icons/teams-alias-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
icons/teams-alias-48.png
Normal file
BIN
icons/teams-alias-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
23
manifest.json
Normal file
23
manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Teams 别名管理",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/teams-alias-16.png",
|
||||||
|
"32": "icons/teams-alias-32.png",
|
||||||
|
"48": "icons/teams-alias-48.png",
|
||||||
|
"128": "icons/teams-alias-128.png"
|
||||||
|
},
|
||||||
|
"description": "给 Teams 好友设置别名",
|
||||||
|
"permissions": ["storage", "scripting"],
|
||||||
|
"host_permissions": ["https://teams.live.com/v2*"],
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["https://teams.live.com/v2*"],
|
||||||
|
"js": ["content.js"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
package-lock.json
generated
Normal file
22
package-lock.json
generated
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "teams-alias",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "teams-alias",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"dependencies": {
|
||||||
|
"chrome-types": "^0.1.347"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chrome-types": {
|
||||||
|
"version": "0.1.347",
|
||||||
|
"resolved": "https://registry.npmjs.org/chrome-types/-/chrome-types-0.1.347.tgz",
|
||||||
|
"integrity": "sha512-c8lbQnYfghYJ8hQ6XivJzex3NJS7RAdcD81uGylnvrRqzF2QMei/wVhsE6+PsZDOipNxbFMxjhf242ZuGhYZzQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
package.json
Normal file
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "teams-alias",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"chrome-types": "^0.1.347"
|
||||||
|
}
|
||||||
|
}
|
||||||
64
popup.html
Normal file
64
popup.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
background-color: #f9fafb;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
width: 120px;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
background-color: #2563eb;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileInput {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<title>Teams 别名管理</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3>Teams 别名管理</h3>
|
||||||
|
<button id="export">导出</button>
|
||||||
|
<button id="import">覆盖导入</button>
|
||||||
|
<button id="update-import">合并导入</button>
|
||||||
|
<input type="file" id="fileInput" accept="application/json" />
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
64
popup.js
Normal file
64
popup.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// 导出数据
|
||||||
|
document.getElementById('export').addEventListener('click', async () => {
|
||||||
|
const { aliases } = await chrome.storage.local.get('aliases');
|
||||||
|
const blob = new Blob([JSON.stringify(aliases || {}, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const timestamp = new Date()
|
||||||
|
.toLocaleString('sv-SE') // ISO-like 格式
|
||||||
|
.replace(' ', '_')
|
||||||
|
.replace(/:/g, '-'); // 避免非法文件名
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `teams-alias-${timestamp}.json`;
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
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