chore: remove chrome-extension from tracking and add to gitignore
This commit is contained in:
+2
-1
@@ -41,4 +41,5 @@ web/
|
||||
**/__pycache__
|
||||
/scripts/scale_dev_jobs.sh
|
||||
server
|
||||
frontend/.vite
|
||||
frontend/.vite
|
||||
frontend/chrome-extension/
|
||||
@@ -1,739 +0,0 @@
|
||||
// background.js — Service Worker
|
||||
// 存储管理 + 消息路由 + 右键菜单 + API 通信
|
||||
|
||||
// === WeKnora API Helper ===
|
||||
// 构建请求头:API Key 使用 X-API-Key header,Bearer token 使用 Authorization header
|
||||
async function buildHeaders(config) {
|
||||
var headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
||||
};
|
||||
if (config.apiKey) {
|
||||
// API Key (sk- 开头) 使用 X-API-Key header 进行租户级认证
|
||||
headers['X-API-Key'] = config.apiKey;
|
||||
}
|
||||
if (config.bearerToken) {
|
||||
// Bearer token (通过用户名密码登录获取) 使用 Authorization header
|
||||
headers['Authorization'] = 'Bearer ' + config.bearerToken;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function apiRequest(method, path, body, options) {
|
||||
var config = await getConfigData();
|
||||
if (!config || !config.baseUrl) {
|
||||
return { success: false, error: '未配置服务地址,请先在设置中配置' };
|
||||
}
|
||||
var baseUrl = config.baseUrl.replace(/\/+$/, '');
|
||||
var url = baseUrl + path;
|
||||
var headers = await buildHeaders(config);
|
||||
try {
|
||||
var fetchOpts = { method: method, headers: headers };
|
||||
if (body && method !== 'GET') {
|
||||
fetchOpts.body = JSON.stringify(body);
|
||||
}
|
||||
if (options && options.signal) {
|
||||
fetchOpts.signal = options.signal;
|
||||
}
|
||||
var resp = await fetch(url, fetchOpts);
|
||||
if (!resp.ok) {
|
||||
var errText = '';
|
||||
try { var errJson = await resp.json(); errText = errJson.error?.message || errJson.message || resp.statusText; } catch (e) { errText = resp.statusText; }
|
||||
return { success: false, error: errText, status: resp.status };
|
||||
}
|
||||
var data = await resp.json();
|
||||
return data;
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message || '网络请求失败' };
|
||||
}
|
||||
}
|
||||
|
||||
// SSE streaming chat request — returns ReadableStream
|
||||
async function apiChatStream(path, body) {
|
||||
var config = await getConfigData();
|
||||
if (!config || !config.baseUrl) {
|
||||
return { success: false, error: '未配置服务地址' };
|
||||
}
|
||||
var baseUrl = config.baseUrl.replace(/\/+$/, '');
|
||||
var url = baseUrl + path;
|
||||
var headers = await buildHeaders(config);
|
||||
headers['Accept'] = 'text/event-stream';
|
||||
try {
|
||||
var resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body),
|
||||
cache: 'no-store' // 避免浏览器缓存导致 SSE 流被缓冲
|
||||
});
|
||||
if (!resp.ok) {
|
||||
var errText = '';
|
||||
try { var errJson = await resp.json(); errText = errJson.error?.message || errJson.message || resp.statusText; } catch (e) { errText = resp.statusText; }
|
||||
return { success: false, error: errText };
|
||||
}
|
||||
return { success: true, response: resp };
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message || '网络请求失败' };
|
||||
}
|
||||
}
|
||||
|
||||
// 流式推送消息到 sidepanel / popup 等前端页面
|
||||
function notifyStream(msg) {
|
||||
chrome.runtime.sendMessage(msg).catch(function () {});
|
||||
}
|
||||
|
||||
// Helper to get raw config data
|
||||
async function getConfigData() {
|
||||
var data = await chrome.storage.local.get('ka_config');
|
||||
return data.ka_config || null;
|
||||
}
|
||||
|
||||
// Session cache per tab for sidepanel chat
|
||||
var chatSessions = {};
|
||||
|
||||
// === 右键菜单 ===
|
||||
// 防止并发注册
|
||||
var _menuSetupInProgress = false;
|
||||
|
||||
function setupContextMenus() {
|
||||
if (_menuSetupInProgress) return;
|
||||
_menuSetupInProgress = true;
|
||||
|
||||
chrome.contextMenus.removeAll(function () {
|
||||
void chrome.runtime.lastError; // 清除可能的 lastError
|
||||
|
||||
// 保存选中文字
|
||||
chrome.contextMenus.create({
|
||||
id: 'ka-save-selection',
|
||||
title: '保存到快速笔记 5.0',
|
||||
contexts: ['selection']
|
||||
}, function () { void chrome.runtime.lastError; });
|
||||
|
||||
// 用选中文字提问
|
||||
chrome.contextMenus.create({
|
||||
id: 'ka-ask-selection',
|
||||
title: '使用知识助理提问',
|
||||
contexts: ['selection']
|
||||
}, function () { void chrome.runtime.lastError; });
|
||||
|
||||
// 保存图片到快速笔记
|
||||
chrome.contextMenus.create({
|
||||
id: 'ka-save-image',
|
||||
title: '保存图片到快速笔记',
|
||||
contexts: ['image']
|
||||
}, function () { void chrome.runtime.lastError; });
|
||||
|
||||
_menuSetupInProgress = false;
|
||||
|
||||
// 根据登录状态更新菜单文案
|
||||
updateContextMenuTitle();
|
||||
});
|
||||
}
|
||||
|
||||
// 插件安装/更新时注册
|
||||
chrome.runtime.onInstalled.addListener(function () {
|
||||
setupContextMenus();
|
||||
});
|
||||
|
||||
// Service Worker 每次启动时也注册(确保重载后菜单存在)
|
||||
setupContextMenus();
|
||||
|
||||
// 根据登录类型动态更新右键菜单中"提问"的文案
|
||||
async function updateContextMenuTitle() {
|
||||
var data = await chrome.storage.local.get('ka_auth');
|
||||
var auth = data.ka_auth;
|
||||
var askTitle = '使用知识助理提问';
|
||||
if (auth && auth.type === 'wk') {
|
||||
askTitle = '使用 WeKnora 提问';
|
||||
} else if (auth && auth.type === 'ka') {
|
||||
askTitle = '使用知识助理提问';
|
||||
}
|
||||
chrome.contextMenus.update('ka-ask-selection', { title: askTitle }, function () {
|
||||
void chrome.runtime.lastError; // 菜单还未创建时静默忽略
|
||||
});
|
||||
}
|
||||
|
||||
chrome.contextMenus.onClicked.addListener(function (info, tab) {
|
||||
// 处理图片保存(不需要 selectionText)
|
||||
if (info.menuItemId === 'ka-save-image') {
|
||||
var imgUrl = info.srcUrl;
|
||||
if (!imgUrl) return;
|
||||
var title = '图片收藏 - ' + (tab.title || '未知页面');
|
||||
var clip = {
|
||||
type: 'image-clip',
|
||||
content: '',
|
||||
title: title,
|
||||
meta: { url: tab.url || '', title: tab.title || '', imageUrl: imgUrl }
|
||||
};
|
||||
saveClip(clip).then(function () {
|
||||
if (tab && tab.id) {
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'SHOW_NOTIFICATION',
|
||||
payload: { msg: '图片已保存到快速笔记', status: 'success' }
|
||||
}).catch(function () {});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!info.selectionText) return;
|
||||
|
||||
if (info.menuItemId === 'ka-save-selection') {
|
||||
var title = '选中文本 - ' + (tab.title || '未知页面');
|
||||
var clip = {
|
||||
type: 'select-clip',
|
||||
content: info.selectionText,
|
||||
title: title,
|
||||
meta: { url: tab.url || '', title: tab.title || '' }
|
||||
};
|
||||
saveClip(clip).then(function () {
|
||||
if (tab && tab.id) {
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'SHOW_NOTIFICATION',
|
||||
payload: { msg: '已保存到快速笔记', status: 'success' }
|
||||
}).catch(function () {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (info.menuItemId === 'ka-ask-selection') {
|
||||
// 打开侧边栏并将选中文字作为问题
|
||||
if (tab && tab.id) {
|
||||
chrome.storage.local.set({
|
||||
ka_pending_query: { query: info.selectionText, ts: Date.now() }
|
||||
});
|
||||
chrome.sidePanel.open({ tabId: tab.id }).catch(function () {});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
|
||||
handleMessage(msg, sender).then(function (result) {
|
||||
sendResponse(result);
|
||||
}).catch(function (err) {
|
||||
sendResponse({ success: false, error: err.message || '未知错误' });
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
async function handleMessage(msg, sender) {
|
||||
switch (msg.type) {
|
||||
case 'GET_AUTH':
|
||||
return getAuth();
|
||||
case 'SET_AUTH':
|
||||
return setAuth(msg.payload);
|
||||
case 'CLEAR_AUTH':
|
||||
return clearAuth();
|
||||
case 'GET_CONFIG':
|
||||
return getConfig();
|
||||
case 'SET_CONFIG':
|
||||
return setConfig(msg.payload);
|
||||
case 'SAVE_NOTE':
|
||||
return saveNote(msg.payload);
|
||||
case 'GET_NOTES':
|
||||
return getNotes();
|
||||
case 'SAVE_CLIP':
|
||||
return saveClip(msg.payload);
|
||||
case 'GET_CLIPS':
|
||||
return getClips();
|
||||
case 'DELETE_CLIP':
|
||||
return deleteClip(msg.payload);
|
||||
case 'DELETE_NOTE':
|
||||
return deleteNote(msg.payload);
|
||||
case 'UPDATE_CLIP':
|
||||
return updateClip(msg.payload);
|
||||
case 'UPDATE_NOTE':
|
||||
return updateNote(msg.payload);
|
||||
case 'INJECT_SCRIPT':
|
||||
return injectScript(msg.payload.tabId);
|
||||
case 'ASK_WEKNORA':
|
||||
// 打开侧边栏并传递选中的文字作为问题
|
||||
if (sender && sender.tab && sender.tab.id) {
|
||||
await chrome.sidePanel.open({ tabId: sender.tab.id });
|
||||
// 存储待处理的问题,sidepanel 加载后会读取
|
||||
await chrome.storage.local.set({
|
||||
ka_pending_query: { query: msg.payload.text, ts: Date.now() }
|
||||
});
|
||||
}
|
||||
return { success: true };
|
||||
case 'SAVE_SELECTION':
|
||||
// 从气泡保存选中文字
|
||||
return saveClip(msg.payload);
|
||||
case 'SAVE_IMAGE':
|
||||
// 从气泡保存图片
|
||||
return saveClip(msg.payload);
|
||||
case 'CAPTURE_SCREENSHOT':
|
||||
// 截取当前标签页可见区域
|
||||
try {
|
||||
var tabId = sender && sender.tab && sender.tab.id;
|
||||
if (!tabId) return { success: false, error: '无法获取标签页' };
|
||||
var dataUrl = await chrome.tabs.captureVisibleTab(null, { format: 'jpeg', quality: 90 });
|
||||
return { success: true, dataUrl: dataUrl };
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message || '截图失败' };
|
||||
}
|
||||
|
||||
// === WeKnora API 相关 ===
|
||||
case 'VALIDATE_CONFIG':
|
||||
// API Key 没有专门的验证端点,通过调用知识库列表接口来验证连通性和认证
|
||||
return apiRequest('GET', '/knowledge-bases');
|
||||
|
||||
case 'LOGIN': {
|
||||
// 用户名密码登录,获取 Bearer token
|
||||
var loginPayload = msg.payload || {};
|
||||
var loginResp = await apiRequest('POST', '/auth/login', {
|
||||
email: loginPayload.email,
|
||||
password: loginPayload.password
|
||||
});
|
||||
// 如果登录成功,将 token 存入 config
|
||||
if (loginResp && loginResp.success && loginResp.token) {
|
||||
var curConfig = await getConfigData() || {};
|
||||
curConfig.bearerToken = loginResp.token;
|
||||
curConfig.refreshToken = loginResp.refresh_token;
|
||||
await chrome.storage.local.set({ ka_config: curConfig });
|
||||
}
|
||||
return loginResp;
|
||||
}
|
||||
|
||||
case 'LIST_KNOWLEDGE_BASES':
|
||||
// agent_id 参数仅用于共享智能体(跨租户),本地/内置智能体不传
|
||||
var agentFilter = (msg.payload && msg.payload.sharedAgentId) ? '?agent_id=' + msg.payload.sharedAgentId : '';
|
||||
return apiRequest('GET', '/knowledge-bases' + agentFilter);
|
||||
|
||||
case 'LIST_AGENTS':
|
||||
return apiRequest('GET', '/agents');
|
||||
|
||||
case 'CREATE_KNOWLEDGE_BASE':
|
||||
return apiRequest('POST', '/knowledge-bases', msg.payload);
|
||||
|
||||
case 'CREATE_SESSION':
|
||||
return apiRequest('POST', '/sessions', msg.payload || {});
|
||||
|
||||
case 'LIST_SESSIONS':
|
||||
var p = msg.payload || {};
|
||||
return apiRequest('GET', '/sessions?page=' + (p.page || 1) + '&page_size=' + (p.page_size || 20));
|
||||
|
||||
case 'CLEAR_SESSION_MESSAGES': {
|
||||
var sid = (msg.payload || {}).sessionId;
|
||||
if (!sid) return { success: false, error: '缺少 sessionId' };
|
||||
return apiRequest('DELETE', '/sessions/' + sid + '/messages');
|
||||
}
|
||||
|
||||
case 'CHAT_QUERY': {
|
||||
// 真正的知识库问答 — 使用 SSE 流式输出
|
||||
var payload = msg.payload || {};
|
||||
var query = payload.query;
|
||||
if (!query) return { success: false, error: '请输入问题' };
|
||||
|
||||
// 获取或创建会话
|
||||
var sessionId = payload.sessionId;
|
||||
if (!sessionId) {
|
||||
var sessionResp = await apiRequest('POST', '/sessions', {});
|
||||
if (sessionResp && sessionResp.success && sessionResp.data) {
|
||||
sessionId = sessionResp.data.id;
|
||||
} else if (sessionResp && sessionResp.id) {
|
||||
sessionId = sessionResp.id;
|
||||
}
|
||||
if (!sessionId) {
|
||||
return { success: false, error: '创建会话失败: ' + (sessionResp.error || '未知错误') };
|
||||
}
|
||||
await chrome.storage.local.set({ ka_current_session: sessionId });
|
||||
}
|
||||
|
||||
// 确定使用知识库问答还是智能体问答
|
||||
var kbIds = payload.knowledgeBaseIds || [];
|
||||
var agentId = payload.agentId || '';
|
||||
var useAgent = payload.agentEnabled || false;
|
||||
var chatPath = useAgent
|
||||
? '/agent-chat/' + sessionId
|
||||
: '/knowledge-chat/' + sessionId;
|
||||
|
||||
// 构建完整请求体,参考 CreateKnowledgeQARequest
|
||||
var chatBody = { query: query };
|
||||
if (kbIds.length > 0) {
|
||||
chatBody.knowledge_base_ids = kbIds;
|
||||
}
|
||||
if (agentId) {
|
||||
chatBody.agent_id = agentId;
|
||||
}
|
||||
if (useAgent) {
|
||||
chatBody.agent_enabled = true;
|
||||
}
|
||||
if (payload.webSearchEnabled) {
|
||||
chatBody.web_search_enabled = true;
|
||||
}
|
||||
if (payload.mentionedItems) {
|
||||
chatBody.mentioned_items = payload.mentionedItems;
|
||||
}
|
||||
if (payload.images && payload.images.length > 0) {
|
||||
chatBody.images = payload.images;
|
||||
}
|
||||
|
||||
// 使用请求 ID 区分不同来源的流式推送
|
||||
var chatRequestId = payload._requestId || (Date.now().toString(36) + Math.random().toString(36).slice(2, 6));
|
||||
|
||||
// SSE 流式请求
|
||||
var streamResult = await apiChatStream(chatPath, chatBody);
|
||||
if (!streamResult.success) {
|
||||
return { success: false, error: streamResult.error };
|
||||
}
|
||||
|
||||
// 读取 SSE 流,逐块推送到前端
|
||||
try {
|
||||
var reader = streamResult.response.body.getReader();
|
||||
var decoder = new TextDecoder();
|
||||
var fullText = '';
|
||||
var buffer = '';
|
||||
|
||||
while (true) {
|
||||
var readResult = await reader.read();
|
||||
if (readResult.done) break;
|
||||
buffer += decoder.decode(readResult.value, { stream: true });
|
||||
|
||||
// SSE 格式: "event: message\ndata: {json}\n\n"
|
||||
// 按双换行分割完整事件块
|
||||
var eventBlocks = buffer.split('\n\n');
|
||||
buffer = eventBlocks.pop() || '';
|
||||
|
||||
for (var bi = 0; bi < eventBlocks.length; bi++) {
|
||||
var block = eventBlocks[bi].trim();
|
||||
if (!block) continue;
|
||||
|
||||
// 从事件块中提取 data 行
|
||||
var dataLine = '';
|
||||
var blockLines = block.split('\n');
|
||||
for (var li = 0; li < blockLines.length; li++) {
|
||||
var bline = blockLines[li];
|
||||
if (bline.startsWith('data:')) {
|
||||
dataLine = bline.substring(5).trim();
|
||||
}
|
||||
}
|
||||
if (!dataLine || dataLine === '[DONE]') continue;
|
||||
|
||||
try {
|
||||
var evt = JSON.parse(dataLine);
|
||||
var responseType = evt.response_type || '';
|
||||
|
||||
// 根据 response_type 处理不同事件
|
||||
if (responseType === 'answer') {
|
||||
var chunk = evt.content || '';
|
||||
if (chunk) {
|
||||
fullText += chunk;
|
||||
notifyStream({
|
||||
type: 'CHAT_STREAM_CHUNK',
|
||||
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'answer', content: chunk, done: !!evt.done }
|
||||
});
|
||||
}
|
||||
} else if (responseType === 'thinking') {
|
||||
notifyStream({
|
||||
type: 'CHAT_STREAM_CHUNK',
|
||||
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'thinking', content: evt.content || '', eventId: evt.data && evt.data.event_id }
|
||||
});
|
||||
} else if (responseType === 'tool_call') {
|
||||
notifyStream({
|
||||
type: 'CHAT_STREAM_CHUNK',
|
||||
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'tool_call', content: evt.content || '', toolName: evt.data && evt.data.tool_name, eventId: evt.data && (evt.data.event_id || evt.data.tool_call_id) }
|
||||
});
|
||||
} else if (responseType === 'tool_result') {
|
||||
notifyStream({
|
||||
type: 'CHAT_STREAM_CHUNK',
|
||||
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'tool_result', content: evt.content || '', toolName: evt.data && evt.data.tool_name, eventId: evt.data && (evt.data.event_id || evt.data.tool_call_id), success: evt.data && evt.data.success }
|
||||
});
|
||||
} else if (responseType === 'references') {
|
||||
var kRefs = evt.knowledge_references;
|
||||
if (Array.isArray(kRefs) && kRefs.length > 0) {
|
||||
notifyStream({
|
||||
type: 'CHAT_STREAM_CHUNK',
|
||||
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'references', references: kRefs }
|
||||
});
|
||||
}
|
||||
} else if (responseType === 'error') {
|
||||
notifyStream({
|
||||
type: 'CHAT_STREAM_CHUNK',
|
||||
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'error', content: evt.content || '请求出错' }
|
||||
});
|
||||
} else if (responseType === 'complete') {
|
||||
notifyStream({
|
||||
type: 'CHAT_STREAM_CHUNK',
|
||||
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'complete', done: true }
|
||||
});
|
||||
}
|
||||
if (responseType === 'session_title') {
|
||||
notifyStream({
|
||||
type: 'CHAT_STREAM_CHUNK',
|
||||
payload: { requestId: chatRequestId, sessionId: sessionId, responseType: 'session_title', content: evt.content || '' }
|
||||
});
|
||||
}
|
||||
// agent_query 等事件静默忽略
|
||||
} catch (e) {
|
||||
// 非 JSON data 行,忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: fullText || '未获取到回复内容', sessionId: sessionId, requestId: chatRequestId };
|
||||
} catch (streamErr) {
|
||||
return { success: false, error: '读取回复流失败: ' + streamErr.message };
|
||||
}
|
||||
}
|
||||
|
||||
case 'SAVE_CLIP_TO_KB': {
|
||||
// 保存剪藏内容到知识库(作为手动知识条目)
|
||||
var pl = msg.payload || {};
|
||||
if (!pl.kbId || !pl.content) return { success: false, error: '缺少知识库 ID 或内容' };
|
||||
var contentWithMeta = pl.content;
|
||||
if (pl.url) {
|
||||
contentWithMeta = '> 来源: ' + pl.url + '\n\n' + pl.content;
|
||||
}
|
||||
return apiRequest('POST', '/knowledge-bases/' + pl.kbId + '/knowledge/manual', {
|
||||
title: pl.title || '快速笔记剪藏',
|
||||
content: contentWithMeta,
|
||||
status: 'publish'
|
||||
});
|
||||
}
|
||||
|
||||
case 'FETCH_FILE': {
|
||||
// 通过 background 代理带认证头请求文件(图片等),返回 data URL
|
||||
var filePath = (msg.payload || {}).filePath;
|
||||
if (!filePath) return { success: false, error: '缺少 filePath' };
|
||||
var cfg = await getConfigData();
|
||||
if (!cfg || !cfg.baseUrl) return { success: false, error: '未配置服务地址' };
|
||||
var fileBaseUrl = cfg.baseUrl.replace(/\/+$/, '').replace(/\/api\/v\d+$/, '');
|
||||
var fileUrl = fileBaseUrl + '/files?file_path=' + encodeURIComponent(filePath);
|
||||
var fileHeaders = await buildHeaders(cfg);
|
||||
try {
|
||||
var fileResp = await fetch(fileUrl, { method: 'GET', headers: fileHeaders });
|
||||
if (!fileResp.ok) return { success: false, error: 'HTTP ' + fileResp.status };
|
||||
var blob = await fileResp.blob();
|
||||
// 转为 data URL 以便跨上下文传递
|
||||
var reader2 = new FileReader();
|
||||
var dataUrl = await new Promise(function (resolve, reject) {
|
||||
reader2.onload = function () { resolve(reader2.result); };
|
||||
reader2.onerror = function () { reject(new Error('FileReader error')); };
|
||||
reader2.readAsDataURL(blob);
|
||||
});
|
||||
return { success: true, dataUrl: dataUrl };
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message || '文件请求失败' };
|
||||
}
|
||||
}
|
||||
|
||||
case 'GET_USER_INFO':
|
||||
return apiRequest('GET', '/auth/me');
|
||||
|
||||
default:
|
||||
return { success: false, error: '未知消息类型' };
|
||||
}
|
||||
}
|
||||
|
||||
// === Auth ===
|
||||
async function getAuth() {
|
||||
var data = await chrome.storage.local.get('ka_auth');
|
||||
return { success: true, data: data.ka_auth || null };
|
||||
}
|
||||
|
||||
async function setAuth(auth) {
|
||||
await chrome.storage.local.set({ ka_auth: auth });
|
||||
updateContextMenuTitle();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function clearAuth() {
|
||||
await chrome.storage.local.remove('ka_auth');
|
||||
updateContextMenuTitle();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// === Config (WeKnora) ===
|
||||
async function getConfig() {
|
||||
var data = await chrome.storage.local.get('ka_config');
|
||||
return { success: true, data: data.ka_config || { baseUrl: '', apiKey: '' } };
|
||||
}
|
||||
|
||||
async function setConfig(config) {
|
||||
await chrome.storage.local.set({ ka_config: config });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// === Notes (Markdown) ===
|
||||
async function saveNote(note) {
|
||||
var data = await chrome.storage.local.get('ka_notes');
|
||||
var notes = data.ka_notes || [];
|
||||
note.id = Date.now().toString();
|
||||
note.createdAt = new Date().toISOString();
|
||||
notes.unshift(note);
|
||||
if (notes.length > 100) notes = notes.slice(0, 100);
|
||||
await chrome.storage.local.set({ ka_notes: notes });
|
||||
return { success: true, data: note };
|
||||
}
|
||||
|
||||
async function getNotes() {
|
||||
var data = await chrome.storage.local.get('ka_notes');
|
||||
return { success: true, data: data.ka_notes || [] };
|
||||
}
|
||||
|
||||
// === Clips (网页截取收藏) ===
|
||||
async function saveClip(clip) {
|
||||
try {
|
||||
var data = await chrome.storage.local.get('ka_clips');
|
||||
var clips = data.ka_clips || [];
|
||||
|
||||
// 如果传入了已有 id,说明是编辑已有笔记,执行更新而非新增
|
||||
if (clip.id) {
|
||||
var found = false;
|
||||
for (var i = 0; i < clips.length; i++) {
|
||||
if (clips[i].id === clip.id) {
|
||||
// 保留原始创建时间和其他元数据,只更新内容相关字段
|
||||
clips[i].content = clip.content;
|
||||
if (clip.title) clips[i].title = clip.title;
|
||||
if (clip.type) clips[i].type = clip.type;
|
||||
clips[i].updatedAt = new Date().toISOString();
|
||||
clip = clips[i]; // 返回完整的更新后记录
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如果在 ka_clips 中没找到,再到 ka_notes 中查找并更新
|
||||
if (!found) {
|
||||
var notesData = await chrome.storage.local.get('ka_notes');
|
||||
var notes = notesData.ka_notes || [];
|
||||
for (var j = 0; j < notes.length; j++) {
|
||||
if (notes[j].id === clip.id) {
|
||||
notes[j].content = clip.content;
|
||||
if (clip.title) notes[j].title = clip.title;
|
||||
if (clip.type) notes[j].type = clip.type;
|
||||
notes[j].updatedAt = new Date().toISOString();
|
||||
clip = notes[j];
|
||||
found = true;
|
||||
await chrome.storage.local.set({ ka_notes: notes });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
await chrome.storage.local.set({ ka_clips: clips });
|
||||
return { success: true, data: clip };
|
||||
}
|
||||
// 没找到原记录,当作新建处理(fallthrough)
|
||||
}
|
||||
|
||||
// 新建记录
|
||||
clip.id = Date.now().toString();
|
||||
clip.createdAt = new Date().toISOString();
|
||||
clips.unshift(clip);
|
||||
if (clips.length > 200) clips = clips.slice(0, 200);
|
||||
await chrome.storage.local.set({ ka_clips: clips });
|
||||
return { success: true, data: clip };
|
||||
} catch (err) {
|
||||
// 如果保存失败(可能是截图太大),尝试去掉截图再保存
|
||||
if (clip.screenshot) {
|
||||
try {
|
||||
delete clip.screenshot;
|
||||
var data2 = await chrome.storage.local.get('ka_clips');
|
||||
var clips2 = data2.ka_clips || [];
|
||||
clips2.unshift(clip);
|
||||
if (clips2.length > 200) clips2 = clips2.slice(0, 200);
|
||||
await chrome.storage.local.set({ ka_clips: clips2 });
|
||||
return { success: true, data: clip, warning: '截图过大已省略,仅保存文字' };
|
||||
} catch (err2) {
|
||||
return { success: false, error: '保存失败: ' + (err2.message || '存储空间不足') };
|
||||
}
|
||||
}
|
||||
return { success: false, error: '保存失败: ' + (err.message || '未知错误') };
|
||||
}
|
||||
}
|
||||
|
||||
async function getClips() {
|
||||
var data = await chrome.storage.local.get('ka_clips');
|
||||
return { success: true, data: data.ka_clips || [] };
|
||||
}
|
||||
|
||||
async function deleteClip(payload) {
|
||||
var data = await chrome.storage.local.get('ka_clips');
|
||||
var clips = data.ka_clips || [];
|
||||
clips = clips.filter(function (c) { return c.id !== payload.id; });
|
||||
await chrome.storage.local.set({ ka_clips: clips });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function deleteNote(payload) {
|
||||
var data = await chrome.storage.local.get('ka_notes');
|
||||
var notes = data.ka_notes || [];
|
||||
notes = notes.filter(function (n) { return n.id !== payload.id; });
|
||||
await chrome.storage.local.set({ ka_notes: notes });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function updateClip(payload) {
|
||||
var data = await chrome.storage.local.get('ka_clips');
|
||||
var clips = data.ka_clips || [];
|
||||
var found = false;
|
||||
for (var i = 0; i < clips.length; i++) {
|
||||
if (clips[i].id === payload.id) {
|
||||
clips[i].content = payload.content;
|
||||
if (payload.title) clips[i].title = payload.title;
|
||||
clips[i].updatedAt = new Date().toISOString();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) return { success: false, error: '未找到对应记录' };
|
||||
await chrome.storage.local.set({ ka_clips: clips });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function updateNote(payload) {
|
||||
var data = await chrome.storage.local.get('ka_notes');
|
||||
var notes = data.ka_notes || [];
|
||||
var found = false;
|
||||
for (var i = 0; i < notes.length; i++) {
|
||||
if (notes[i].id === payload.id) {
|
||||
notes[i].content = payload.content;
|
||||
if (payload.title) notes[i].title = payload.title;
|
||||
notes[i].updatedAt = new Date().toISOString();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) return { success: false, error: '未找到对应记录' };
|
||||
await chrome.storage.local.set({ ka_notes: notes });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// === Inject content script ===
|
||||
async function injectScript(tabId) {
|
||||
try {
|
||||
await chrome.scripting.executeScript({ target: { tabId: tabId }, files: ['content.js'] });
|
||||
await chrome.scripting.insertCSS({ target: { tabId: tabId }, files: ['content.css'] });
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
// === Commands ===
|
||||
chrome.commands.onCommand.addListener(async function (cmd, tab) {
|
||||
if (!tab || !tab.id) return;
|
||||
if (cmd === 'open-sidepanel') {
|
||||
await chrome.sidePanel.open({ tabId: tab.id });
|
||||
}
|
||||
if (cmd === 'quick-ask') {
|
||||
await chrome.sidePanel.open({ tabId: tab.id });
|
||||
}
|
||||
if (cmd === 'select-clip') {
|
||||
// 快捷键触发选择剪藏:先确保 content script 已注入,再发消息
|
||||
try {
|
||||
await chrome.tabs.sendMessage(tab.id, { type: 'SELECT_CLIP' });
|
||||
} catch (e) {
|
||||
// content script 未注入,先注入再发送
|
||||
try {
|
||||
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
|
||||
await chrome.scripting.insertCSS({ target: { tabId: tab.id }, files: ['content.css'] });
|
||||
setTimeout(function () {
|
||||
chrome.tabs.sendMessage(tab.id, { type: 'SELECT_CLIP' }).catch(function () {});
|
||||
}, 300);
|
||||
} catch (injectErr) {
|
||||
// 无法注入的页面(如 chrome:// 页面),忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,623 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>网页内容收藏 - 快速笔记 5.0</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root { --primary: #07C160; --primary-dark: #06ad54; --bg: #f5f6f7; --white: #fff; --text: #1a1a1a; --text2: #555; --text3: #999; --border: #eaeaea; --sidebar-w: 220px; }
|
||||
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; }
|
||||
|
||||
/* 顶部导航 */
|
||||
.top-bar { background: var(--white); border-bottom: 1px solid var(--border); padding: 0 32px; height: 56px; display: flex; align-items: center; gap: 16px; position: sticky; top: 0; z-index: 10; flex-shrink: 0; }
|
||||
.top-bar-logo { display: flex; align-items: center; gap: 10px; }
|
||||
.top-bar-logo-icon { width: 32px; height: 32px; background: var(--primary); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 15px; font-weight: 700; }
|
||||
.top-bar-logo-icon svg { width: 18px; height: 18px; color: #fff; }
|
||||
.top-bar-logo h1 { font-size: 16px; font-weight: 700; color: var(--text); }
|
||||
.top-bar-divider { width: 1px; height: 20px; background: var(--border); }
|
||||
.top-bar-title { font-size: 14px; color: var(--text2); }
|
||||
.top-bar-spacer { flex: 1; }
|
||||
.top-bar-actions { display: flex; gap: 8px; }
|
||||
.top-bar-btn { height: 34px; padding: 0 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--white); font-size: 13px; color: var(--text2); cursor: pointer; display: flex; align-items: center; gap: 6px; transition: all 0.15s; }
|
||||
.top-bar-btn:hover { border-color: var(--primary); color: var(--primary); }
|
||||
.top-bar-btn svg { width: 14px; height: 14px; }
|
||||
|
||||
/* ===== 主体左右布局 ===== */
|
||||
.main-layout { flex: 1; display: flex; overflow: hidden; }
|
||||
|
||||
/* 左侧分类栏 */
|
||||
.sidebar { width: var(--sidebar-w); background: var(--white); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; overflow-y: auto; }
|
||||
.sidebar-title { font-size: 11px; font-weight: 600; color: var(--text3); letter-spacing: 0.5px; text-transform: uppercase; padding: 20px 20px 10px; }
|
||||
.sidebar-list { list-style: none; padding: 0 8px; flex: 1; }
|
||||
.sidebar-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; cursor: pointer; font-size: 13px; color: var(--text2); transition: all 0.15s; margin-bottom: 2px; user-select: none; }
|
||||
.sidebar-item:hover { background: #f5f5f5; color: var(--text); }
|
||||
.sidebar-item.active { background: rgba(7, 193, 96, 0.08); color: var(--primary); font-weight: 600; }
|
||||
.sidebar-item .si-icon { width: 32px; height: 32px; border-radius: 9px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: all 0.15s; }
|
||||
.sidebar-item .si-icon svg { width: 16px; height: 16px; }
|
||||
.sidebar-item.active .si-icon { background: rgba(7, 193, 96, 0.12); }
|
||||
.sidebar-item .si-icon-all { background: #f0f0f0; color: var(--text3); }
|
||||
.sidebar-item.active .si-icon-all { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
|
||||
.sidebar-item .si-icon-select { background: #f0f7ff; color: #5b9bd5; }
|
||||
.sidebar-item.active .si-icon-select { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
|
||||
.sidebar-item .si-icon-smart { background: #fff7ed; color: #e8913a; }
|
||||
.sidebar-item.active .si-icon-smart { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
|
||||
.sidebar-item .si-icon-markdown { background: #f0f0ff; color: #6c6ce0; }
|
||||
.sidebar-item.active .si-icon-markdown { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
|
||||
.sidebar-item .si-label { flex: 1; }
|
||||
.sidebar-item .si-count { font-size: 11px; color: var(--text3); background: #f0f0f0; padding: 1px 7px; border-radius: 10px; font-weight: 500; min-width: 22px; text-align: center; transition: all 0.15s; }
|
||||
.sidebar-item.active .si-count { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
|
||||
|
||||
/* 右侧内容区 */
|
||||
.content-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
|
||||
/* 内容顶部工具栏 */
|
||||
.content-toolbar { display: flex; align-items: center; gap: 12px; padding: 18px 28px 0; flex-shrink: 0; }
|
||||
.content-toolbar-title { font-size: 18px; font-weight: 700; color: var(--text); }
|
||||
.content-toolbar-count { font-size: 13px; color: var(--text3); font-weight: 400; margin-left: 2px; }
|
||||
.content-toolbar-spacer { flex: 1; }
|
||||
.content-search { height: 36px; width: 240px; border: 1px solid var(--border); border-radius: 8px; padding: 0 12px 0 34px; font-size: 13px; outline: none; transition: border-color 0.2s; background: var(--white) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E") 12px center no-repeat; }
|
||||
.content-search:focus { border-color: var(--primary); }
|
||||
|
||||
/* 内容滚动区 */
|
||||
.content-scroll { flex: 1; overflow-y: auto; padding: 16px 28px 40px; }
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state { text-align: center; padding: 80px 20px; }
|
||||
.empty-state-icon { width: 80px; height: 80px; background: #e8f8ee; border-radius: 20px; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; }
|
||||
.empty-state-icon svg { width: 36px; height: 36px; color: var(--primary); }
|
||||
.empty-state h2 { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
|
||||
.empty-state p { font-size: 14px; color: var(--text3); line-height: 1.6; }
|
||||
|
||||
/* 收藏卡片列表 — 网格卡片布局 */
|
||||
.clip-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
|
||||
|
||||
.clip-card {
|
||||
background: var(--white);
|
||||
border-radius: 16px;
|
||||
border: none;
|
||||
overflow: visible;
|
||||
transition: all 0.25s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 200px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06), 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
.clip-card:hover {
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.clip-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 20px 0;
|
||||
}
|
||||
.clip-card-favicon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
background: #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.clip-card-favicon svg { width: 12px; height: 12px; color: var(--text3); }
|
||||
.clip-card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.clip-card-time {
|
||||
font-size: 11px;
|
||||
color: var(--text3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.clip-card-body {
|
||||
padding: 10px 20px 8px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.clip-card-content {
|
||||
font-size: 13px;
|
||||
color: var(--text2);
|
||||
line-height: 1.65;
|
||||
max-height: 140px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
cursor: pointer;
|
||||
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 65%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, #000 0%, #000 65%, transparent 100%);
|
||||
}
|
||||
.clip-card-content.expanded {
|
||||
max-height: none;
|
||||
-webkit-mask-image: none;
|
||||
mask-image: none;
|
||||
}
|
||||
.clip-card-fade {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.clip-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 20px 12px;
|
||||
margin-top: auto;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
.clip-card-tag {
|
||||
font-size: 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(7, 193, 96, 0.08);
|
||||
color: var(--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.clip-card-footer-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.clip-card-actions { position: relative; flex-shrink: 0; }
|
||||
.clip-more-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text3);
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.clip-more-btn:hover { color: var(--text); background: #f5f5f5; }
|
||||
.clip-more-btn svg { width: 18px; height: 18px; }
|
||||
.clip-more-menu {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
margin-bottom: 4px;
|
||||
background: var(--white);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 6px 24px rgba(0,0,0,0.15);
|
||||
padding: 4px 0;
|
||||
min-width: 120px;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
}
|
||||
.clip-more-menu.show { display: block; }
|
||||
/* 打开菜单时提升当前卡片层级,避免被相邻卡片遮挡 */
|
||||
.clip-card.menu-open {
|
||||
z-index: 60;
|
||||
position: relative;
|
||||
}
|
||||
.clip-more-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
transition: all 0.12s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.clip-more-menu-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
|
||||
.clip-more-menu-item.menu-delete:hover { background: #fff0f0; color: #e44; }
|
||||
.clip-more-menu-item svg { width: 14px; height: 14px; flex-shrink: 0; }
|
||||
|
||||
/* 编辑态时卡片跨满一行 */
|
||||
.clip-card.editing {
|
||||
grid-column: 1 / -1;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
/* === 编辑遮罩(黑色半透明蒙版) === */
|
||||
.edit-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 300;
|
||||
display: none;
|
||||
animation: edit-overlay-in 0.25s ease;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
.edit-overlay.show { display: block; }
|
||||
@keyframes edit-overlay-in { from { opacity: 0; } to { opacity: 1; } }
|
||||
|
||||
/* === 编辑浮层卡片 === */
|
||||
.clip-card.editing {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 310;
|
||||
width: 92vw;
|
||||
max-width: 1100px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
border-color: transparent;
|
||||
animation: edit-card-in 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
}
|
||||
@keyframes edit-card-in {
|
||||
from { opacity: 0; transform: translate(-50%, -46%) scale(0.95); }
|
||||
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||
}
|
||||
.clip-card.editing:hover {
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* === 选择剪藏卡片 — 截图缩略图 === */
|
||||
.clip-screenshot-thumb {
|
||||
width: calc(100% - 28px);
|
||||
margin: 6px 14px 0;
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
max-height: 180px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.clip-screenshot-thumb:hover {
|
||||
opacity: 0.92;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 0 1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* === 选择剪藏编辑态 — 左图右编辑器双栏 === */
|
||||
.clip-edit-split {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 16px 18px 0;
|
||||
}
|
||||
.clip-edit-split-left {
|
||||
flex: 0 0 45%;
|
||||
max-width: 45%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.clip-edit-split-left img {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
object-fit: contain;
|
||||
max-height: 500px;
|
||||
background: #f8f8f8;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
.clip-edit-split-left img:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 0 1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.clip-edit-split-left .split-label {
|
||||
font-size: 11px;
|
||||
color: var(--text3);
|
||||
text-align: center;
|
||||
}
|
||||
.clip-edit-split-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.clip-edit-split-right .clip-edit-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.clip-edit-split-right .clip-edit-textarea {
|
||||
flex: 1;
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
/* 速记标签色 */
|
||||
.clip-card-tag.tag-markdown { background: rgba(59,125,216,0.1); color: #3b7dd8; }
|
||||
.clip-card-tag.tag-image { background: rgba(233,89,80,0.1); color: #e95950; }
|
||||
.clip-card-tag.tag-text { background: rgba(134,142,150,0.1); color: #868e96; }
|
||||
|
||||
/* 图片分类图标 */
|
||||
.sidebar-item .si-icon-image { background: #fef0f0; color: #e95950; }
|
||||
.sidebar-item.active .si-icon-image { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
|
||||
|
||||
/* Markdown 渲染样式 */
|
||||
.md-rendered { white-space: normal; }
|
||||
.md-rendered h1 { font-size: 20px; font-weight: 700; margin: 0 0 10px; color: var(--text); border-bottom: 1px solid var(--border); padding-bottom: 6px; }
|
||||
.md-rendered h2 { font-size: 17px; font-weight: 700; margin: 14px 0 8px; color: var(--text); }
|
||||
.md-rendered h3 { font-size: 15px; font-weight: 600; margin: 12px 0 6px; color: var(--text); }
|
||||
.md-rendered h4 { font-size: 14px; font-weight: 600; margin: 10px 0 4px; color: var(--text); }
|
||||
.md-rendered p { margin: 6px 0; line-height: 1.7; }
|
||||
.md-rendered ul, .md-rendered ol { margin: 6px 0; padding-left: 20px; }
|
||||
.md-rendered li { margin: 3px 0; line-height: 1.6; }
|
||||
.md-rendered code { font-family: "SF Mono","Fira Code","Menlo",monospace; background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; color: #d63384; }
|
||||
.md-rendered pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 8px 0; font-size: 12px; line-height: 1.5; }
|
||||
.md-rendered pre code { background: none; padding: 0; color: inherit; font-size: inherit; }
|
||||
.md-rendered blockquote { border-left: 3px solid var(--primary); padding: 6px 14px; margin: 8px 0; background: #f8faf8; color: var(--text2); font-size: 13px; border-radius: 0 6px 6px 0; }
|
||||
.md-rendered hr { border: none; border-top: 1px solid var(--border); margin: 12px 0; }
|
||||
.md-rendered a { color: var(--primary); text-decoration: none; }
|
||||
.md-rendered a:hover { text-decoration: underline; }
|
||||
.md-rendered strong { font-weight: 700; }
|
||||
.md-rendered em { font-style: italic; }
|
||||
.md-rendered table { width: 100%; border-collapse: collapse; margin: 8px 0; font-size: 13px; }
|
||||
.md-rendered th, .md-rendered td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
|
||||
.md-rendered th { background: #f5f6f7; font-weight: 600; }
|
||||
.md-rendered img { max-width: 100%; border-radius: 6px; margin: 6px 0; }
|
||||
|
||||
/* 编辑态 */
|
||||
.clip-card-edit-area { display: none; padding: 0 18px 14px; }
|
||||
.clip-card.editing .clip-card-header { display: none; }
|
||||
.clip-card.editing .clip-card-body { display: none; }
|
||||
.clip-card.editing .clip-card-footer { display: none; }
|
||||
.clip-card.editing .clip-card-edit-area { display: block; }
|
||||
/* 有分栏布局时,padding 由分栏自身控制 */
|
||||
.clip-card.editing .clip-card-edit-area:has(.clip-edit-split) { padding: 0 0 14px; }
|
||||
|
||||
/* 编辑浮层标题栏 */
|
||||
.clip-edit-header {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 20px 8px;
|
||||
}
|
||||
.clip-card.editing .clip-edit-header { display: flex; }
|
||||
.clip-edit-header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
flex: 1;
|
||||
}
|
||||
.clip-edit-header-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #f0f0f0;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text3);
|
||||
transition: all 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.clip-edit-header-close:hover { background: #e5e5e5; color: var(--text); }
|
||||
.clip-edit-header-close svg { width: 16px; height: 16px; }
|
||||
|
||||
.clip-edit-card {
|
||||
background: #f8fcf9;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
border: 1.5px solid #edf8f1;
|
||||
}
|
||||
.clip-edit-textarea {
|
||||
width: 100%;
|
||||
min-height: 300px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 14px 16px;
|
||||
font-family: "SF Mono","Fira Code","Menlo","Consolas",monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
resize: none;
|
||||
outline: none;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.clip-edit-textarea::placeholder { color: #c0c0c0; }
|
||||
|
||||
/* 编辑工具栏 */
|
||||
.clip-edit-toolbar { display: flex; align-items: center; padding: 2px 8px; gap: 0; border-top: 1px solid #edf8f1; }
|
||||
.clip-edit-toolbar-group { display: flex; align-items: center; gap: 0; }
|
||||
.clip-edit-toolbar-divider { width: 1px; height: 14px; background: #e0e8e2; margin: 0 6px; flex-shrink: 0; }
|
||||
.clip-edit-tool-btn { width: 28px; height: 28px; border: none; background: transparent; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #999; transition: all 0.15s; padding: 0; }
|
||||
.clip-edit-tool-btn:hover { background: rgba(7,193,96,0.08); color: #555; }
|
||||
.clip-edit-tool-btn:active { background: rgba(7,193,96,0.15); }
|
||||
.clip-edit-tool-btn svg { width: 15px; height: 15px; }
|
||||
.clip-edit-toolbar-spacer { flex: 1; }
|
||||
.clip-edit-actions { display: flex; align-items: center; gap: 6px; }
|
||||
.clip-edit-btn {
|
||||
height: 28px;
|
||||
padding: 0 14px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.clip-edit-btn.cancel {
|
||||
background: #eee;
|
||||
color: var(--text2);
|
||||
}
|
||||
.clip-edit-btn.cancel:hover { background: #e0e0e0; color: var(--text); }
|
||||
.clip-edit-btn.save {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
.clip-edit-btn.save:hover { background: var(--primary-dark); box-shadow: 0 2px 8px rgba(7,193,96,0.25); }
|
||||
.clip-edit-btn.save:disabled { background: #ddd; box-shadow: none; cursor: not-allowed; }
|
||||
.clip-edit-tool-btn.clip-preview-active { background: rgba(7,193,96,0.12); color: var(--primary); }
|
||||
|
||||
/* Toast */
|
||||
.toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%) translateY(-80px); background: var(--text); color: #fff; padding: 10px 22px; border-radius: 22px; font-size: 13px; z-index: 200; transition: transform 0.3s ease; pointer-events: none; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
||||
.toast.show { transform: translateX(-50%) translateY(0); }
|
||||
.toast.success { background: var(--primary); }
|
||||
|
||||
/* === 图片灯箱 Lightbox === */
|
||||
.lightbox-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
z-index: 500;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: zoom-out;
|
||||
animation: lightbox-fadein 0.2s ease;
|
||||
}
|
||||
.lightbox-overlay.show { display: flex; }
|
||||
@keyframes lightbox-fadein { from { opacity: 0; } to { opacity: 1; } }
|
||||
.lightbox-img {
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
border-radius: 0;
|
||||
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.4);
|
||||
cursor: default;
|
||||
object-fit: contain;
|
||||
animation: lightbox-scalein 0.25s ease;
|
||||
}
|
||||
@keyframes lightbox-scalein { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 28px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.lightbox-close:hover { background: rgba(255, 255, 255, 0.3); }
|
||||
.lightbox-close svg { width: 20px; height: 20px; }
|
||||
/* 让编辑态截图有可点击放大提示 */
|
||||
.clip-edit-split-left img { cursor: zoom-in; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="top-bar">
|
||||
<div class="top-bar-logo">
|
||||
<div class="top-bar-logo-icon">S</div>
|
||||
<h1>快速笔记 5.0</h1>
|
||||
</div>
|
||||
<div class="top-bar-divider"></div>
|
||||
<span class="top-bar-title">内容收藏</span>
|
||||
<div class="top-bar-spacer"></div>
|
||||
<div class="top-bar-actions">
|
||||
<button class="top-bar-btn" id="btn-refresh">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
刷新
|
||||
</button>
|
||||
<button class="top-bar-btn" id="btn-clear-all">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
清空全部
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<!-- 左侧分类栏 -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-title">分类</div>
|
||||
<ul class="sidebar-list">
|
||||
<li class="sidebar-item active" data-filter="all">
|
||||
<div class="si-icon si-icon-all">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
||||
</div>
|
||||
<span class="si-label">全部内容</span>
|
||||
<span class="si-count" id="count-all">0</span>
|
||||
</li>
|
||||
<li class="sidebar-item" data-filter="select-clip">
|
||||
<div class="si-icon si-icon-select">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3h5v2H5v3H3V3zm13 0h5v5h-2V5h-3V3zM3 16v5h5v-2H5v-3H3zm18 0v5h-5v-2h3v-3h2z"/></svg>
|
||||
</div>
|
||||
<span class="si-label">截图</span>
|
||||
<span class="si-count" id="count-select">0</span>
|
||||
</li>
|
||||
<li class="sidebar-item" data-filter="smart-clip">
|
||||
<div class="si-icon si-icon-smart">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
||||
</div>
|
||||
<span class="si-label">智能剪藏</span>
|
||||
<span class="si-count" id="count-smart">0</span>
|
||||
</li>
|
||||
<li class="sidebar-item" data-filter="markdown">
|
||||
<div class="si-icon si-icon-markdown">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
</div>
|
||||
<span class="si-label">速记</span>
|
||||
<span class="si-count" id="count-markdown">0</span>
|
||||
</li>
|
||||
<li class="sidebar-item" data-filter="image-clip">
|
||||
<div class="si-icon si-icon-image">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
</div>
|
||||
<span class="si-label">图片收藏</span>
|
||||
<span class="si-count" id="count-image">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="content-area">
|
||||
<div class="content-toolbar">
|
||||
<span class="content-toolbar-title" id="content-title">全部内容</span>
|
||||
<span class="content-toolbar-count">(<strong id="clips-total">0</strong> 条)</span>
|
||||
<div class="content-toolbar-spacer"></div>
|
||||
<input type="text" class="content-search" id="search-input" placeholder="搜索收藏内容...">
|
||||
</div>
|
||||
<div class="content-scroll">
|
||||
<div class="clip-list" id="clip-list">
|
||||
<!-- 动态生成 -->
|
||||
</div>
|
||||
<div class="empty-state" id="empty-state" style="display:none;">
|
||||
<div class="empty-state-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
|
||||
</div>
|
||||
<h2>暂无收藏内容</h2>
|
||||
<p>使用插件的「选择剪藏」或「智能剪藏」功能<br>截取网页内容后会自动保存到这里</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的筛选 select(供 JS 兼容使用) -->
|
||||
<select id="filter-type" style="display:none;">
|
||||
<option value="all">全部类型</option>
|
||||
<option value="select-clip">截图</option>
|
||||
<option value="smart-clip">智能剪藏</option>
|
||||
<option value="markdown">速记笔记</option>
|
||||
<option value="image-clip">图片收藏</option>
|
||||
</select>
|
||||
|
||||
<!-- 编辑态背景遮罩 -->
|
||||
<div class="edit-overlay" id="edit-overlay"></div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<!-- 图片灯箱 -->
|
||||
<div class="lightbox-overlay" id="lightbox">
|
||||
<button class="lightbox-close" id="lightbox-close" title="关闭">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
<img class="lightbox-img" id="lightbox-img" src="" alt="原图预览">
|
||||
</div>
|
||||
|
||||
<script src="clips.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,746 +0,0 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
var clipList = $('clip-list');
|
||||
var emptyState = $('empty-state');
|
||||
var totalEl = $('clips-total');
|
||||
var searchInput = $('search-input');
|
||||
var filterType = $('filter-type');
|
||||
var contentTitle = $('content-title');
|
||||
var allClips = []; // 每条记录增加 _source 字段: 'ka_clips' | 'ka_notes'
|
||||
|
||||
// === 图片灯箱 ===
|
||||
var lightbox = $('lightbox');
|
||||
var lightboxImg = $('lightbox-img');
|
||||
var lightboxClose = $('lightbox-close');
|
||||
|
||||
function openLightbox(src) {
|
||||
lightboxImg.src = src;
|
||||
lightbox.classList.add('show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeLightbox() {
|
||||
lightbox.classList.remove('show');
|
||||
lightboxImg.src = '';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 点击遮罩关闭(不点击图片本身)
|
||||
lightbox.addEventListener('click', function (e) {
|
||||
if (e.target === lightbox) closeLightbox();
|
||||
});
|
||||
lightboxClose.addEventListener('click', closeLightbox);
|
||||
// ESC 关闭
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && lightbox.classList.contains('show')) {
|
||||
closeLightbox();
|
||||
}
|
||||
});
|
||||
|
||||
// 分类名称映射
|
||||
var categoryLabels = {
|
||||
'all': '全部内容',
|
||||
'select-clip': '截图',
|
||||
'smart-clip': '智能剪藏',
|
||||
'markdown': '速记笔记',
|
||||
'image-clip': '图片收藏'
|
||||
};
|
||||
|
||||
// === Toast ===
|
||||
function toast(msg, type) {
|
||||
var el = $('toast');
|
||||
el.textContent = msg;
|
||||
el.className = 'toast show' + (type ? ' ' + type : '');
|
||||
setTimeout(function () { el.classList.remove('show'); }, 2000);
|
||||
}
|
||||
|
||||
// === 轻量级 Markdown → HTML 渲染 ===
|
||||
function renderMarkdown(text) {
|
||||
if (!text) return '';
|
||||
var html = text;
|
||||
|
||||
// 代码块 (```...```) — 先处理,避免内部被其他规则干扰
|
||||
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
|
||||
return '<pre><code>' + escapeHtml(code.replace(/\n$/, '')) + '</code></pre>';
|
||||
});
|
||||
|
||||
// 行内代码
|
||||
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
// 表格(简单支持)
|
||||
html = html.replace(/((?:\|.+\|\n)+)/g, function (tableBlock) {
|
||||
var rows = tableBlock.trim().split('\n');
|
||||
if (rows.length < 2) return tableBlock;
|
||||
var out = '<table>';
|
||||
rows.forEach(function (row, i) {
|
||||
// 跳过分隔行 |---|---|
|
||||
if (/^\|[\s\-:|]+\|$/.test(row)) return;
|
||||
var tag = i === 0 ? 'th' : 'td';
|
||||
var cells = row.split('|').filter(function (c, ci, arr) { return ci > 0 && ci < arr.length - 1; });
|
||||
out += '<tr>';
|
||||
cells.forEach(function (cell) {
|
||||
out += '<' + tag + '>' + cell.trim() + '</' + tag + '>';
|
||||
});
|
||||
out += '</tr>';
|
||||
});
|
||||
out += '</table>';
|
||||
return out;
|
||||
});
|
||||
|
||||
// 将剩余文本按行处理
|
||||
var lines = html.split('\n');
|
||||
var result = [];
|
||||
var inList = false;
|
||||
var listType = '';
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i];
|
||||
|
||||
// 已渲染的 <pre>/<table> 块直接保留
|
||||
if (line.indexOf('<pre>') !== -1 || line.indexOf('<table>') !== -1) {
|
||||
if (inList) { result.push('</' + listType + '>'); inList = false; }
|
||||
result.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 标题
|
||||
if (/^####\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h4>' + RegExp.$1 + '</h4>'); continue; }
|
||||
if (/^###\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h3>' + RegExp.$1 + '</h3>'); continue; }
|
||||
if (/^##\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h2>' + RegExp.$1 + '</h2>'); continue; }
|
||||
if (/^#\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h1>' + RegExp.$1 + '</h1>'); continue; }
|
||||
|
||||
// 分隔线
|
||||
if (/^---+$/.test(line.trim())) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<hr>'); continue; }
|
||||
|
||||
// 引用
|
||||
if (/^>\s*(.*)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<blockquote>' + RegExp.$1 + '</blockquote>'); continue; }
|
||||
|
||||
// 无序列表
|
||||
if (/^[\-\*]\s+(.+)/.test(line)) {
|
||||
if (!inList || listType !== 'ul') {
|
||||
if (inList) result.push('</' + listType + '>');
|
||||
result.push('<ul>'); inList = true; listType = 'ul';
|
||||
}
|
||||
result.push('<li>' + RegExp.$1 + '</li>');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 有序列表
|
||||
if (/^\d+\.\s+(.+)/.test(line)) {
|
||||
if (!inList || listType !== 'ol') {
|
||||
if (inList) result.push('</' + listType + '>');
|
||||
result.push('<ol>'); inList = true; listType = 'ol';
|
||||
}
|
||||
result.push('<li>' + RegExp.$1 + '</li>');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 其他:关闭列表,空行跳过,非空行包裹 <p>
|
||||
if (inList) { result.push('</' + listType + '>'); inList = false; }
|
||||
if (line.trim() === '') { continue; }
|
||||
result.push('<p>' + line + '</p>');
|
||||
}
|
||||
if (inList) result.push('</' + listType + '>');
|
||||
|
||||
html = result.join('\n');
|
||||
|
||||
// 行内样式(在所有块级元素处理完之后)
|
||||
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// === 加载收藏列表 ===
|
||||
function loadClips() {
|
||||
chrome.runtime.sendMessage({ type: 'GET_CLIPS' }, function (resp) {
|
||||
if (resp && resp.success) {
|
||||
allClips = (resp.data || []).map(function (c) { c._source = 'ka_clips'; return c; });
|
||||
// 同时加载 ka_notes 中的旧数据(向后兼容)
|
||||
chrome.runtime.sendMessage({ type: 'GET_NOTES' }, function (notesResp) {
|
||||
if (notesResp && notesResp.success && notesResp.data) {
|
||||
notesResp.data.forEach(function (note) {
|
||||
// 避免重复(相同 id 的数据已在 ka_clips 中)
|
||||
var exists = allClips.some(function (c) { return c.id === note.id; });
|
||||
if (!exists) {
|
||||
note._source = 'ka_notes';
|
||||
allClips.push(note);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 按时间排序
|
||||
allClips.sort(function (a, b) {
|
||||
return new Date(b.createdAt || 0) - new Date(a.createdAt || 0);
|
||||
});
|
||||
renderClips();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// === 编辑工具栏辅助函数 ===
|
||||
function clipInsertMd(textarea, before, after, placeholder) {
|
||||
textarea.focus();
|
||||
var start = textarea.selectionStart;
|
||||
var end = textarea.selectionEnd;
|
||||
var text = textarea.value;
|
||||
var selected = text.substring(start, end);
|
||||
var insert = selected || placeholder || '';
|
||||
textarea.value = text.substring(0, start) + before + insert + (after || '') + text.substring(end);
|
||||
textarea.selectionStart = start + before.length;
|
||||
textarea.selectionEnd = start + before.length + insert.length;
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
function clipInsertLinePrefix(textarea, prefix) {
|
||||
textarea.focus();
|
||||
var start = textarea.selectionStart;
|
||||
var text = textarea.value;
|
||||
var lineStart = text.lastIndexOf('\n', start - 1) + 1;
|
||||
textarea.value = text.substring(0, lineStart) + prefix + text.substring(lineStart);
|
||||
textarea.selectionStart = textarea.selectionEnd = start + prefix.length;
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
}
|
||||
|
||||
// === 渲染列表 ===
|
||||
function renderClips() {
|
||||
var keyword = searchInput.value.trim().toLowerCase();
|
||||
var typeFilter = filterType.value;
|
||||
|
||||
// 更新右侧标题
|
||||
contentTitle.textContent = categoryLabels[typeFilter] || '全部内容';
|
||||
|
||||
// 更新左侧分类计数
|
||||
updateCategoryCounts();
|
||||
|
||||
var filtered = allClips.filter(function (clip) {
|
||||
if (typeFilter !== 'all' && clip.type !== typeFilter) return false;
|
||||
if (keyword) {
|
||||
var text = ((clip.title || '') + ' ' + (clip.content || '')).toLowerCase();
|
||||
return text.indexOf(keyword) !== -1;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
totalEl.textContent = filtered.length;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
clipList.innerHTML = '';
|
||||
emptyState.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
// Markdown 编辑工具栏 HTML(复用)
|
||||
function buildEditToolbarHtml() {
|
||||
return '<div class="clip-edit-toolbar">'
|
||||
+ ' <div class="clip-edit-toolbar-group">'
|
||||
+ ' <button class="clip-edit-tool-btn" data-md="bold" title="加粗 **文本**"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/></svg></button>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="clip-edit-toolbar-divider"></div>'
|
||||
+ ' <div class="clip-edit-toolbar-group">'
|
||||
+ ' <button class="clip-edit-tool-btn" data-md="heading" title="标题 ## 文本"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4H5z"/></svg></button>'
|
||||
+ ' <button class="clip-edit-tool-btn" data-md="ul" title="无序列表 - 项目"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg></button>'
|
||||
+ ' <button class="clip-edit-tool-btn" data-md="ol" title="有序列表 1. 项目"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg></button>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="clip-edit-toolbar-divider"></div>'
|
||||
+ ' <div class="clip-edit-toolbar-group">'
|
||||
+ ' <button class="clip-edit-tool-btn" data-md="link" title="插入链接 [文本](url)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>'
|
||||
+ ' <button class="clip-edit-tool-btn" data-md="image" title="插入图片 "><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg></button>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="clip-edit-toolbar-divider"></div>'
|
||||
+ ' <div class="clip-edit-toolbar-group">'
|
||||
+ ' <button class="clip-edit-tool-btn" data-md="preview" title="预览 Markdown 效果"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="clip-edit-toolbar-spacer"></div>'
|
||||
+ ' <div class="clip-edit-actions">'
|
||||
+ ' <button class="clip-edit-btn cancel">取消</button>'
|
||||
+ ' <button class="clip-edit-btn save">保存</button>'
|
||||
+ ' </div>'
|
||||
+ ' </div>';
|
||||
}
|
||||
|
||||
var html = '';
|
||||
filtered.forEach(function (clip) {
|
||||
var url = (clip.meta && clip.meta.url) || '';
|
||||
var time = formatTime(clip.createdAt);
|
||||
var isMarkdown = clip.type === 'markdown';
|
||||
var isSmartClip = clip.type === 'smart-clip';
|
||||
var isImageClip = clip.type === 'image-clip';
|
||||
var isSelectClip = clip.type === 'select-clip';
|
||||
var hasScreenshot = isSelectClip && clip.screenshot;
|
||||
|
||||
// 类型标签
|
||||
var typeBadge, tagClass;
|
||||
if (isMarkdown) {
|
||||
typeBadge = '速记';
|
||||
tagClass = 'clip-card-tag tag-markdown';
|
||||
} else if (isSmartClip) {
|
||||
typeBadge = '智能剪藏';
|
||||
tagClass = 'clip-card-tag';
|
||||
} else if (isImageClip) {
|
||||
typeBadge = '图片收藏';
|
||||
tagClass = 'clip-card-tag tag-image';
|
||||
} else if (hasScreenshot) {
|
||||
typeBadge = '截图';
|
||||
tagClass = 'clip-card-tag';
|
||||
} else {
|
||||
typeBadge = '文本';
|
||||
tagClass = 'clip-card-tag tag-text';
|
||||
}
|
||||
|
||||
// 内容区
|
||||
var contentHtml;
|
||||
if (hasScreenshot) {
|
||||
// 选择剪藏有截图:显示缩略图
|
||||
contentHtml = '<img class="clip-screenshot-thumb" src="' + clip.screenshot + '" alt="截图">';
|
||||
} else {
|
||||
// 所有非截图内容统一使用 markdown 渲染(速记笔记、智能剪藏、选中文字保存、图片收藏)
|
||||
contentHtml = '<div class="clip-card-content md-rendered">' + renderMarkdown(clip.content || '') + '</div>';
|
||||
}
|
||||
|
||||
// 编辑区标题栏
|
||||
var editHeaderHtml = '<div class="clip-edit-header">'
|
||||
+ '<span class="clip-edit-header-title">编辑 · ' + escapeHtml(typeBadge) + '</span>'
|
||||
+ '<button class="clip-edit-header-close" title="关闭"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>'
|
||||
+ '</div>';
|
||||
|
||||
// 编辑区:选择剪藏有截图时用左右分栏布局
|
||||
var editAreaHtml;
|
||||
if (hasScreenshot) {
|
||||
editAreaHtml = '<div class="clip-card-edit-area">'
|
||||
+ editHeaderHtml
|
||||
+ '<div class="clip-edit-split">'
|
||||
+ ' <div class="clip-edit-split-left">'
|
||||
+ ' <img src="' + clip.screenshot + '" alt="截图原图">'
|
||||
+ ' <span class="split-label">截图预览(点击放大)</span>'
|
||||
+ ' </div>'
|
||||
+ ' <div class="clip-edit-split-right">'
|
||||
+ ' <div class="clip-edit-card">'
|
||||
+ ' <textarea class="clip-edit-textarea" placeholder="# 标题 在这里编辑截图中的文字…"></textarea>'
|
||||
+ buildEditToolbarHtml()
|
||||
+ ' </div>'
|
||||
+ ' </div>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
} else {
|
||||
editAreaHtml = '<div class="clip-card-edit-area">'
|
||||
+ editHeaderHtml
|
||||
+ ' <div class="clip-edit-card">'
|
||||
+ ' <textarea class="clip-edit-textarea" placeholder="# 标题 在这里编辑内容…"></textarea>'
|
||||
+ buildEditToolbarHtml()
|
||||
+ ' </div>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
html += '<div class="clip-card" data-id="' + clip.id + '" data-source="' + (clip._source || 'ka_clips') + '">'
|
||||
+ '<div class="clip-card-header">'
|
||||
+ ' <span class="clip-card-time">' + time + '</span>'
|
||||
+ '</div>'
|
||||
+ '<div class="clip-card-body">'
|
||||
+ ' ' + contentHtml
|
||||
+ '</div>'
|
||||
+ editAreaHtml
|
||||
+ '<div class="clip-card-footer">'
|
||||
+ ' <span class="' + tagClass + '">' + typeBadge + '</span>'
|
||||
+ ' <div class="clip-card-footer-spacer"></div>'
|
||||
+ ' <div class="clip-card-actions">'
|
||||
+ ' <button class="clip-more-btn" title="更多操作"><svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg></button>'
|
||||
+ ' <div class="clip-more-menu">'
|
||||
+ (url ? ' <button class="clip-more-menu-item menu-open-link" data-url="' + escapeHtml(url) + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>打开链接</button>' : '')
|
||||
+ ' <button class="clip-more-menu-item menu-edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>编辑</button>'
|
||||
+ ' <button class="clip-more-menu-item menu-copy"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>复制</button>'
|
||||
+ ' <button class="clip-more-menu-item menu-delete"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>删除</button>'
|
||||
+ ' </div>'
|
||||
+ ' </div>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
});
|
||||
|
||||
clipList.innerHTML = html;
|
||||
|
||||
// 绑定三点菜单
|
||||
clipList.querySelectorAll('.clip-more-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
var menu = btn.nextElementSibling;
|
||||
var card = btn.closest('.clip-card');
|
||||
// 先关闭其他已打开的菜单
|
||||
clipList.querySelectorAll('.clip-more-menu.show').forEach(function (m) {
|
||||
if (m !== menu) {
|
||||
m.classList.remove('show');
|
||||
var otherCard = m.closest('.clip-card');
|
||||
if (otherCard) otherCard.classList.remove('menu-open');
|
||||
}
|
||||
});
|
||||
// 切换当前菜单
|
||||
var isOpen = menu.classList.toggle('show');
|
||||
if (isOpen) {
|
||||
card.classList.add('menu-open');
|
||||
} else {
|
||||
card.classList.remove('menu-open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 点击其他地方关闭菜单
|
||||
document.addEventListener('click', function () {
|
||||
clipList.querySelectorAll('.clip-more-menu.show').forEach(function (m) {
|
||||
m.classList.remove('show');
|
||||
var card = m.closest('.clip-card');
|
||||
if (card) card.classList.remove('menu-open');
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定复制事件
|
||||
clipList.querySelectorAll('.menu-copy').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var card = btn.closest('.clip-card');
|
||||
var id = card.dataset.id;
|
||||
var clip = allClips.find(function (c) { return c.id === id; });
|
||||
if (clip) {
|
||||
navigator.clipboard.writeText(clip.content || '').then(function () {
|
||||
toast('已复制到剪贴板', 'success');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定删除事件
|
||||
clipList.querySelectorAll('.menu-delete').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var card = btn.closest('.clip-card');
|
||||
var id = card.dataset.id;
|
||||
var source = card.dataset.source;
|
||||
if (confirm('确定要删除这条收藏吗?')) {
|
||||
var msgType = source === 'ka_notes' ? 'DELETE_NOTE' : 'DELETE_CLIP';
|
||||
chrome.runtime.sendMessage({ type: msgType, payload: { id: id } }, function () {
|
||||
allClips = allClips.filter(function (c) { return c.id !== id; });
|
||||
renderClips();
|
||||
toast('已删除', 'success');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定打开链接事件
|
||||
clipList.querySelectorAll('.menu-open-link').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var linkUrl = btn.getAttribute('data-url');
|
||||
if (linkUrl) {
|
||||
window.open(linkUrl, '_blank');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 绑定编辑事件
|
||||
clipList.querySelectorAll('.menu-edit').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var card = btn.closest('.clip-card');
|
||||
enterEditMode(card);
|
||||
});
|
||||
});
|
||||
|
||||
// 双击卡片内容进入编辑模式
|
||||
clipList.querySelectorAll('.clip-card').forEach(function (card) {
|
||||
card.addEventListener('dblclick', function (e) {
|
||||
// 如果已在编辑态,或点击的是按钮/链接/菜单,不触发
|
||||
if (card.classList.contains('editing')) return;
|
||||
var tag = e.target.tagName;
|
||||
if (tag === 'BUTTON' || tag === 'A' || tag === 'TEXTAREA' || tag === 'INPUT') return;
|
||||
if (e.target.closest('.clip-card-actions')) return;
|
||||
if (e.target.closest('.clip-more-menu')) return;
|
||||
enterEditMode(card);
|
||||
});
|
||||
});
|
||||
|
||||
// 点击卡片内容展开/收起(非 markdown 类型才有折叠效果)
|
||||
clipList.querySelectorAll('.clip-card-content:not(.md-rendered)').forEach(function (el) {
|
||||
el.addEventListener('click', function () {
|
||||
el.classList.toggle('expanded');
|
||||
var fade = el.querySelector('.clip-card-fade');
|
||||
if (fade) fade.style.display = el.classList.contains('expanded') ? 'none' : '';
|
||||
});
|
||||
});
|
||||
|
||||
// Markdown 卡片点击展开/收起(无 fade 渐变)
|
||||
clipList.querySelectorAll('.clip-card-content.md-rendered').forEach(function (el) {
|
||||
el.addEventListener('click', function () {
|
||||
el.classList.toggle('expanded');
|
||||
});
|
||||
});
|
||||
|
||||
// 编辑态截图 → 点击打开灯箱查看原图(列表缩略图不放大)
|
||||
clipList.querySelectorAll('.clip-edit-split-left img').forEach(function (img) {
|
||||
img.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
if (img.src) openLightbox(img.src);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// === 进入编辑模式(独立函数,供菜单和双击调用) ===
|
||||
var editOverlay = $('edit-overlay');
|
||||
var _editingCard = null; // 当前编辑中的卡片
|
||||
|
||||
function exitEditMode() {
|
||||
if (_editingCard) {
|
||||
_editingCard.classList.remove('editing');
|
||||
_editingCard = null;
|
||||
}
|
||||
editOverlay.classList.remove('show');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 点击遮罩退出编辑
|
||||
editOverlay.addEventListener('click', exitEditMode);
|
||||
|
||||
// ESC 退出编辑
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && _editingCard) {
|
||||
// 如果灯箱正在显示,优先关闭灯箱
|
||||
if (lightbox.classList.contains('show')) return;
|
||||
exitEditMode();
|
||||
}
|
||||
});
|
||||
|
||||
function enterEditMode(card) {
|
||||
if (!card || card.classList.contains('editing')) return;
|
||||
var id = card.dataset.id;
|
||||
var clip = allClips.find(function (c) { return c.id === id; });
|
||||
if (!clip) return;
|
||||
|
||||
// 关闭其他菜单
|
||||
clipList.querySelectorAll('.clip-more-menu.show').forEach(function (m) {
|
||||
m.classList.remove('show');
|
||||
var c = m.closest('.clip-card');
|
||||
if (c) c.classList.remove('menu-open');
|
||||
});
|
||||
|
||||
// 如果已有其他卡片在编辑,先退出
|
||||
if (_editingCard && _editingCard !== card) {
|
||||
_editingCard.classList.remove('editing');
|
||||
}
|
||||
|
||||
// 显示遮罩 + 进入编辑态
|
||||
_editingCard = card;
|
||||
editOverlay.classList.add('show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
card.classList.add('editing');
|
||||
|
||||
var textarea = card.querySelector('.clip-edit-textarea');
|
||||
textarea.value = clip.content || '';
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.max(300, textarea.scrollHeight) + 'px';
|
||||
textarea.focus();
|
||||
|
||||
// 输入时自动调整高度
|
||||
textarea.oninput = function () {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.max(300, textarea.scrollHeight) + 'px';
|
||||
};
|
||||
|
||||
// 标题栏关闭按钮
|
||||
var headerClose = card.querySelector('.clip-edit-header-close');
|
||||
if (headerClose) {
|
||||
headerClose.onclick = function () { exitEditMode(); };
|
||||
}
|
||||
|
||||
// 工具栏按钮事件
|
||||
card.querySelectorAll('.clip-edit-tool-btn[data-md]').forEach(function (toolBtn) {
|
||||
toolBtn.onclick = function (e) {
|
||||
e.preventDefault();
|
||||
var action = toolBtn.getAttribute('data-md');
|
||||
switch (action) {
|
||||
case 'bold':
|
||||
clipInsertMd(textarea, '**', '**', '粗体文本');
|
||||
break;
|
||||
case 'heading':
|
||||
clipInsertLinePrefix(textarea, '## ');
|
||||
break;
|
||||
case 'ul':
|
||||
clipInsertLinePrefix(textarea, '- ');
|
||||
break;
|
||||
case 'ol':
|
||||
clipInsertLinePrefix(textarea, '1. ');
|
||||
break;
|
||||
case 'link':
|
||||
clipInsertMd(textarea, '[', '](url)', '链接文本');
|
||||
break;
|
||||
case 'image':
|
||||
clipInsertMd(textarea, '', '图片描述');
|
||||
break;
|
||||
case 'preview':
|
||||
if (!textarea.value.trim() && textarea.style.display !== 'none') { toast('请先输入内容'); return; }
|
||||
var previewArea = card.querySelector('.clip-edit-preview');
|
||||
if (!previewArea) {
|
||||
previewArea = document.createElement('div');
|
||||
previewArea.className = 'clip-edit-preview md-rendered';
|
||||
previewArea.style.cssText = 'padding:12px 14px;font-size:13px;line-height:1.7;color:#555;overflow-y:auto;min-height:120px;display:none;';
|
||||
textarea.parentNode.insertBefore(previewArea, textarea.nextSibling);
|
||||
}
|
||||
var isPreviewing = textarea.style.display === 'none';
|
||||
if (isPreviewing) {
|
||||
textarea.style.display = '';
|
||||
previewArea.style.display = 'none';
|
||||
toolBtn.classList.remove('clip-preview-active');
|
||||
textarea.focus();
|
||||
} else {
|
||||
previewArea.innerHTML = renderMarkdown(textarea.value);
|
||||
textarea.style.display = 'none';
|
||||
previewArea.style.display = 'block';
|
||||
toolBtn.classList.add('clip-preview-active');
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 取消按钮
|
||||
var cancelBtn = card.querySelector('.clip-edit-btn.cancel');
|
||||
cancelBtn.onclick = function () {
|
||||
exitEditMode();
|
||||
};
|
||||
|
||||
// 保存按钮
|
||||
var saveBtn = card.querySelector('.clip-edit-btn.save');
|
||||
saveBtn.onclick = function () {
|
||||
var newContent = textarea.value;
|
||||
var source = card.dataset.source;
|
||||
var storageKey = source === 'ka_notes' ? 'ka_notes' : 'ka_clips';
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = '保存中...';
|
||||
|
||||
chrome.storage.local.get(storageKey, function (data) {
|
||||
var items = data[storageKey] || [];
|
||||
var found = false;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].id === id) {
|
||||
items[i].content = newContent;
|
||||
items[i].updatedAt = new Date().toISOString();
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '保存';
|
||||
toast('未找到对应记录', '');
|
||||
return;
|
||||
}
|
||||
var setData = {};
|
||||
setData[storageKey] = items;
|
||||
chrome.storage.local.set(setData, function () {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '保存';
|
||||
if (chrome.runtime.lastError) {
|
||||
toast('保存失败,请重试', '');
|
||||
return;
|
||||
}
|
||||
clip.content = newContent;
|
||||
if (clip.type === 'markdown') {
|
||||
clip.title = newContent.split('\n')[0].replace(/^#+\s*/, '').substring(0, 50) || '未命名笔记';
|
||||
}
|
||||
exitEditMode();
|
||||
renderClips();
|
||||
toast('已保存', 'success');
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// === 工具函数 ===
|
||||
function escapeHtml(s) {
|
||||
var d = document.createElement('div');
|
||||
d.textContent = s || '';
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function formatTime(iso) {
|
||||
if (!iso) return '';
|
||||
var d = new Date(iso);
|
||||
var now = new Date();
|
||||
var diff = now - d;
|
||||
|
||||
if (diff < 60000) return '刚刚';
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + ' 分钟前';
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + ' 小时前';
|
||||
if (diff < 604800000) return Math.floor(diff / 86400000) + ' 天前';
|
||||
|
||||
var m = d.getMonth() + 1;
|
||||
var day = d.getDate();
|
||||
var h = d.getHours().toString().padStart(2, '0');
|
||||
var min = d.getMinutes().toString().padStart(2, '0');
|
||||
return m + '月' + day + '日 ' + h + ':' + min;
|
||||
}
|
||||
|
||||
// === 分类计数更新 ===
|
||||
function updateCategoryCounts() {
|
||||
var countAll = allClips.length;
|
||||
var countSelect = 0;
|
||||
var countSmart = 0;
|
||||
var countMarkdown = 0;
|
||||
var countImage = 0;
|
||||
allClips.forEach(function (c) {
|
||||
if (c.type === 'select-clip') countSelect++;
|
||||
else if (c.type === 'smart-clip') countSmart++;
|
||||
else if (c.type === 'markdown') countMarkdown++;
|
||||
else if (c.type === 'image-clip') countImage++;
|
||||
});
|
||||
$('count-all').textContent = countAll;
|
||||
$('count-select').textContent = countSelect;
|
||||
$('count-smart').textContent = countSmart;
|
||||
$('count-markdown').textContent = countMarkdown;
|
||||
if ($('count-image')) $('count-image').textContent = countImage;
|
||||
}
|
||||
|
||||
// === 左侧分类点击 ===
|
||||
document.querySelectorAll('.sidebar-item').forEach(function (item) {
|
||||
item.addEventListener('click', function () {
|
||||
document.querySelectorAll('.sidebar-item').forEach(function (i) { i.classList.remove('active'); });
|
||||
item.classList.add('active');
|
||||
filterType.value = item.getAttribute('data-filter');
|
||||
renderClips();
|
||||
});
|
||||
});
|
||||
|
||||
// === 事件绑定 ===
|
||||
searchInput.addEventListener('input', renderClips);
|
||||
|
||||
$('btn-refresh').addEventListener('click', function () {
|
||||
loadClips();
|
||||
toast('已刷新', 'success');
|
||||
});
|
||||
|
||||
$('btn-clear-all').addEventListener('click', function () {
|
||||
if (allClips.length === 0) return;
|
||||
if (confirm('确定要清空所有收藏吗?此操作不可恢复。')) {
|
||||
// 同时清空 ka_clips 和 ka_notes
|
||||
chrome.storage.local.set({ ka_clips: [], ka_notes: [] }, function () {
|
||||
allClips = [];
|
||||
renderClips();
|
||||
toast('已清空', 'success');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// === 监听 storage 变化,实时刷新列表 ===
|
||||
// 当其他页面保存了新内容时,列表自动更新
|
||||
if (chrome && chrome.storage && chrome.storage.onChanged) {
|
||||
chrome.storage.onChanged.addListener(function (changes, area) {
|
||||
if (area === 'local' && (changes.ka_clips || changes.ka_notes)) {
|
||||
loadClips();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// === 初始化 ===
|
||||
loadClips();
|
||||
})();
|
||||
@@ -1,261 +0,0 @@
|
||||
/* 快速笔记 5.0 - 内容脚本样式 */
|
||||
|
||||
/* === 截屏式区域框选 === */
|
||||
#ka-clip-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2147483646;
|
||||
cursor: crosshair;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
/* 暗幕遮罩(四块拼合) */
|
||||
.ka-clip-dim {
|
||||
position: fixed;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* 选区矩形 */
|
||||
.ka-clip-selection {
|
||||
position: fixed;
|
||||
border: 2px solid #07C160;
|
||||
background: transparent;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 0 0 1px rgba(7, 193, 96, 0.3), 0 0 20px rgba(7, 193, 96, 0.1);
|
||||
}
|
||||
|
||||
/* 选区角标尺寸拖拽点 */
|
||||
.ka-clip-handle {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #fff;
|
||||
border: 2px solid #07C160;
|
||||
border-radius: 2px;
|
||||
z-index: 2;
|
||||
pointer-events: auto !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ka-clip-handle-nw { top: -6px; left: -6px; cursor: nw-resize; }
|
||||
.ka-clip-handle-n { top: -6px; left: 50%; margin-left: -5px; cursor: n-resize; }
|
||||
.ka-clip-handle-ne { top: -6px; right: -6px; cursor: ne-resize; }
|
||||
.ka-clip-handle-e { top: 50%; margin-top: -5px; right: -6px; cursor: e-resize; }
|
||||
.ka-clip-handle-se { bottom: -6px; right: -6px; cursor: se-resize; }
|
||||
.ka-clip-handle-s { bottom: -6px; left: 50%; margin-left: -5px; cursor: s-resize; }
|
||||
.ka-clip-handle-sw { bottom: -6px; left: -6px; cursor: sw-resize; }
|
||||
.ka-clip-handle-w { top: 50%; margin-top: -5px; left: -6px; cursor: w-resize; }
|
||||
|
||||
/* 提示文字 */
|
||||
.ka-clip-tip {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
color: #fff;
|
||||
padding: 14px 28px;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
backdrop-filter: blur(4px);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 确认工具栏 */
|
||||
.ka-clip-toolbar {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 3;
|
||||
pointer-events: auto;
|
||||
animation: ka-fade-in 0.15s ease;
|
||||
}
|
||||
|
||||
@keyframes ka-fade-in {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.ka-clip-size {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-family: -apple-system, "SF Mono", "Menlo", monospace;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.ka-clip-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.ka-clip-btn kbd {
|
||||
font-size: 10px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.ka-clip-btn-cancel {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #555;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.ka-clip-btn-cancel:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.ka-clip-btn-cancel kbd {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.ka-clip-btn-confirm {
|
||||
background: #07C160;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 12px rgba(7, 193, 96, 0.35);
|
||||
}
|
||||
|
||||
.ka-clip-btn-confirm:hover {
|
||||
background: #06ad54;
|
||||
box-shadow: 0 4px 16px rgba(7, 193, 96, 0.4);
|
||||
}
|
||||
|
||||
/* === 通知 === */
|
||||
.ka-notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 24px;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
z-index: 2147483647;
|
||||
transform: translateX(120%);
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.ka-notification-show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.ka-notification-success {
|
||||
background: #07C160;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ka-notification-error {
|
||||
background: #e44;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ka-notification-info {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* === 选中文字气泡 === */
|
||||
.ka-sel-bubble {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
background: #07C160;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 6px 24px rgba(7, 193, 96, 0.3), 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 2147483645;
|
||||
padding: 4px;
|
||||
animation: ka-bubble-in 0.18s ease;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
@keyframes ka-bubble-in {
|
||||
from { opacity: 0; transform: translateY(6px) scale(0.95); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
.ka-sel-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.15s;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ka-sel-btn svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
flex-shrink: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ka-sel-btn-save:hover {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.ka-sel-btn-ask:hover {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.ka-sel-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 气泡下方小三角(默认朝下) */
|
||||
.ka-sel-arrow {
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #07C160;
|
||||
transform: rotate(45deg);
|
||||
box-shadow: 3px 3px 4px rgba(0, 0, 0, 0.06);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* 气泡在选区下方时,三角朝上 */
|
||||
.ka-sel-arrow.ka-sel-arrow-top {
|
||||
bottom: auto;
|
||||
top: -6px;
|
||||
box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
@@ -1,876 +0,0 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// 防止重复注入 — 用版本号区分
|
||||
var KA_VERSION = 4;
|
||||
if (window.__kaContentVersion === KA_VERSION) {
|
||||
// 已注入过相同版本,但确保消息监听器仍然存在
|
||||
if (typeof window.ensureMessageListener === 'function') {
|
||||
window.ensureMessageListener();
|
||||
}
|
||||
return;
|
||||
}
|
||||
window.__kaContentVersion = KA_VERSION;
|
||||
|
||||
// === 智能剪藏 ===
|
||||
function smartClip() {
|
||||
var content = extractMainContent();
|
||||
var title = document.title || location.hostname;
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'SAVE_CLIP',
|
||||
payload: {
|
||||
type: 'smart-clip',
|
||||
content: '## ' + title + '\n\n' + content + '\n\n---\n来源: ' + location.href,
|
||||
title: '智能剪藏 - ' + title,
|
||||
meta: { url: location.href, title: title }
|
||||
}
|
||||
}, function (resp) {
|
||||
if (resp && resp.success) {
|
||||
showNotification('智能剪藏成功', 'success');
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'CLIP_RESULT',
|
||||
payload: { title: '智能剪藏 - ' + title, content: content }
|
||||
});
|
||||
} else {
|
||||
showNotification('剪藏失败', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function extractMainContent() {
|
||||
var selectors = ['article', 'main', '[role="main"]', '.post-content', '.article-content', '.entry-content', '.content', '#content'];
|
||||
var el = null;
|
||||
for (var i = 0; i < selectors.length; i++) {
|
||||
el = document.querySelector(selectors[i]);
|
||||
if (el && el.textContent.trim().length > 100) break;
|
||||
el = null;
|
||||
}
|
||||
|
||||
if (!el) {
|
||||
var paras = document.querySelectorAll('p');
|
||||
var texts = [];
|
||||
paras.forEach(function (p) {
|
||||
var t = p.textContent.trim();
|
||||
if (t.length > 30) texts.push(t);
|
||||
});
|
||||
return texts.join('\n\n') || document.body.innerText.substring(0, 3000);
|
||||
}
|
||||
|
||||
var clone = el.cloneNode(true);
|
||||
var removes = clone.querySelectorAll('script, style, nav, header, footer, .ads, .ad, .advertisement, .sidebar, .comment, .comments, [role="navigation"], [role="banner"]');
|
||||
removes.forEach(function (r) { r.remove(); });
|
||||
|
||||
return clone.textContent.trim().substring(0, 5000);
|
||||
}
|
||||
|
||||
// === 选择剪藏 — 截屏式区域框选 ===
|
||||
var clipState = {
|
||||
active: false,
|
||||
overlay: null,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
rect: null,
|
||||
toolbar: null,
|
||||
dimTop: null,
|
||||
dimBottom: null,
|
||||
dimLeft: null,
|
||||
dimRight: null
|
||||
};
|
||||
|
||||
function startSelectClip() {
|
||||
if (clipState.active) return;
|
||||
clipState.active = true;
|
||||
|
||||
// 创建全屏遮罩
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'ka-clip-overlay';
|
||||
overlay.innerHTML =
|
||||
'<div class="ka-clip-dim ka-clip-dim-top"></div>' +
|
||||
'<div class="ka-clip-dim ka-clip-dim-bottom"></div>' +
|
||||
'<div class="ka-clip-dim ka-clip-dim-left"></div>' +
|
||||
'<div class="ka-clip-dim ka-clip-dim-right"></div>' +
|
||||
'<div class="ka-clip-selection"></div>' +
|
||||
'<div class="ka-clip-tip">拖拽鼠标框选要截取的区域,松开后确认</div>';
|
||||
document.body.appendChild(overlay);
|
||||
clipState.overlay = overlay;
|
||||
|
||||
clipState.dimTop = overlay.querySelector('.ka-clip-dim-top');
|
||||
clipState.dimBottom = overlay.querySelector('.ka-clip-dim-bottom');
|
||||
clipState.dimLeft = overlay.querySelector('.ka-clip-dim-left');
|
||||
clipState.dimRight = overlay.querySelector('.ka-clip-dim-right');
|
||||
clipState.rect = overlay.querySelector('.ka-clip-selection');
|
||||
|
||||
// 初始暗幕覆盖全屏
|
||||
var w = window.innerWidth;
|
||||
var h = window.innerHeight;
|
||||
setDims(0, 0, w, h);
|
||||
clipState.rect.style.display = 'none';
|
||||
|
||||
overlay.addEventListener('mousedown', onClipMouseDown);
|
||||
document.addEventListener('keydown', onClipKeyDown);
|
||||
}
|
||||
|
||||
function setDims(x, y, x2, y2) {
|
||||
var w = window.innerWidth;
|
||||
var h = window.innerHeight;
|
||||
// 确保有效范围
|
||||
var left = Math.min(x, x2);
|
||||
var top = Math.min(y, y2);
|
||||
var right = Math.max(x, x2);
|
||||
var bottom = Math.max(y, y2);
|
||||
|
||||
// 如果选区大小为0,全屏暗幕
|
||||
if (right - left < 2 || bottom - top < 2) {
|
||||
clipState.dimTop.style.cssText = 'top:0;left:0;width:' + w + 'px;height:' + h + 'px;';
|
||||
clipState.dimBottom.style.cssText = 'display:none;';
|
||||
clipState.dimLeft.style.cssText = 'display:none;';
|
||||
clipState.dimRight.style.cssText = 'display:none;';
|
||||
return;
|
||||
}
|
||||
|
||||
// 上方暗幕
|
||||
clipState.dimTop.style.cssText = 'top:0;left:0;width:' + w + 'px;height:' + top + 'px;';
|
||||
// 下方暗幕
|
||||
clipState.dimBottom.style.cssText = 'top:' + bottom + 'px;left:0;width:' + w + 'px;height:' + (h - bottom) + 'px;';
|
||||
// 左侧暗幕
|
||||
clipState.dimLeft.style.cssText = 'top:' + top + 'px;left:0;width:' + left + 'px;height:' + (bottom - top) + 'px;';
|
||||
// 右侧暗幕
|
||||
clipState.dimRight.style.cssText = 'top:' + top + 'px;left:' + right + 'px;width:' + (w - right) + 'px;height:' + (bottom - top) + 'px;';
|
||||
}
|
||||
|
||||
function onClipMouseDown(e) {
|
||||
if (e.button !== 0) return;
|
||||
// 如果点击在工具栏或 handle 上,不要重新框选
|
||||
if (e.target.closest && (e.target.closest('.ka-clip-toolbar') || e.target.closest('.ka-clip-handle'))) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 移除之前的工具栏和 handles
|
||||
if (clipState.toolbar) {
|
||||
clipState.toolbar.remove();
|
||||
clipState.toolbar = null;
|
||||
}
|
||||
var oldHandles = clipState.overlay.querySelectorAll('.ka-clip-handle');
|
||||
oldHandles.forEach(function (h) { h.remove(); });
|
||||
|
||||
clipState.startX = e.clientX;
|
||||
clipState.startY = e.clientY;
|
||||
clipState.rect.style.display = 'block';
|
||||
clipState.rect.style.left = e.clientX + 'px';
|
||||
clipState.rect.style.top = e.clientY + 'px';
|
||||
clipState.rect.style.width = '0';
|
||||
clipState.rect.style.height = '0';
|
||||
|
||||
// 隐藏提示
|
||||
var tip = clipState.overlay.querySelector('.ka-clip-tip');
|
||||
if (tip) tip.style.display = 'none';
|
||||
|
||||
document.addEventListener('mousemove', onClipMouseMove);
|
||||
document.addEventListener('mouseup', onClipMouseUp);
|
||||
}
|
||||
|
||||
function onClipMouseMove(e) {
|
||||
e.preventDefault();
|
||||
var x = Math.min(e.clientX, clipState.startX);
|
||||
var y = Math.min(e.clientY, clipState.startY);
|
||||
var w = Math.abs(e.clientX - clipState.startX);
|
||||
var h = Math.abs(e.clientY - clipState.startY);
|
||||
|
||||
clipState.rect.style.left = x + 'px';
|
||||
clipState.rect.style.top = y + 'px';
|
||||
clipState.rect.style.width = w + 'px';
|
||||
clipState.rect.style.height = h + 'px';
|
||||
|
||||
setDims(x, y, x + w, y + h);
|
||||
}
|
||||
|
||||
function onClipMouseUp(e) {
|
||||
document.removeEventListener('mousemove', onClipMouseMove);
|
||||
document.removeEventListener('mouseup', onClipMouseUp);
|
||||
|
||||
var x = Math.min(e.clientX, clipState.startX);
|
||||
var y = Math.min(e.clientY, clipState.startY);
|
||||
var w = Math.abs(e.clientX - clipState.startX);
|
||||
var h = Math.abs(e.clientY - clipState.startY);
|
||||
|
||||
if (w < 10 || h < 10) {
|
||||
// 太小,忽略
|
||||
clipState.rect.style.display = 'none';
|
||||
setDims(0, 0, window.innerWidth, window.innerHeight);
|
||||
var tip = clipState.overlay.querySelector('.ka-clip-tip');
|
||||
if (tip) tip.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// 允许拖拽调整大小 — 添加 resize handles
|
||||
addResizeHandles(x, y, w, h);
|
||||
|
||||
// 显示确认工具栏
|
||||
showClipToolbar(x, y, w, h);
|
||||
}
|
||||
|
||||
function addResizeHandles(x, y, w, h) {
|
||||
// 给选区添加 8 个拖拽点
|
||||
var handles = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
|
||||
handles.forEach(function (pos) {
|
||||
var handle = document.createElement('div');
|
||||
handle.className = 'ka-clip-handle ka-clip-handle-' + pos;
|
||||
handle.dataset.pos = pos;
|
||||
clipState.rect.appendChild(handle);
|
||||
|
||||
handle.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
startResize(pos, e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function startResize(pos, e) {
|
||||
var startX = e.clientX;
|
||||
var startY = e.clientY;
|
||||
var origLeft = parseInt(clipState.rect.style.left);
|
||||
var origTop = parseInt(clipState.rect.style.top);
|
||||
var origW = parseInt(clipState.rect.style.width);
|
||||
var origH = parseInt(clipState.rect.style.height);
|
||||
|
||||
function onMove(ev) {
|
||||
ev.preventDefault();
|
||||
var dx = ev.clientX - startX;
|
||||
var dy = ev.clientY - startY;
|
||||
var newLeft = origLeft, newTop = origTop, newW = origW, newH = origH;
|
||||
|
||||
if (pos.indexOf('e') !== -1) newW = Math.max(20, origW + dx);
|
||||
if (pos.indexOf('w') !== -1) { newW = Math.max(20, origW - dx); newLeft = origLeft + dx; }
|
||||
if (pos.indexOf('s') !== -1) newH = Math.max(20, origH + dy);
|
||||
if (pos.indexOf('n') !== -1) { newH = Math.max(20, origH - dy); newTop = origTop + dy; }
|
||||
|
||||
clipState.rect.style.left = newLeft + 'px';
|
||||
clipState.rect.style.top = newTop + 'px';
|
||||
clipState.rect.style.width = newW + 'px';
|
||||
clipState.rect.style.height = newH + 'px';
|
||||
|
||||
setDims(newLeft, newTop, newLeft + newW, newTop + newH);
|
||||
|
||||
// 更新工具栏位置
|
||||
if (clipState.toolbar) {
|
||||
clipState.toolbar.style.left = newLeft + 'px';
|
||||
clipState.toolbar.style.top = (newTop + newH + 8) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', onClipMouseMove);
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
function showClipToolbar(x, y, w, h) {
|
||||
if (clipState.toolbar) clipState.toolbar.remove();
|
||||
|
||||
var bar = document.createElement('div');
|
||||
bar.className = 'ka-clip-toolbar';
|
||||
bar.style.left = x + 'px';
|
||||
bar.style.top = (y + h + 8) + 'px';
|
||||
|
||||
// 选区尺寸提示
|
||||
bar.innerHTML =
|
||||
'<span class="ka-clip-size">' + Math.round(w) + ' × ' + Math.round(h) + '</span>' +
|
||||
'<button class="ka-clip-btn ka-clip-btn-cancel">取消 <kbd>Esc</kbd></button>' +
|
||||
'<button class="ka-clip-btn ka-clip-btn-confirm">确认截取 <kbd>↵</kbd></button>';
|
||||
|
||||
clipState.overlay.appendChild(bar);
|
||||
clipState.toolbar = bar;
|
||||
|
||||
bar.querySelector('.ka-clip-btn-cancel').addEventListener('click', cancelClip);
|
||||
bar.querySelector('.ka-clip-btn-confirm').addEventListener('click', confirmClip);
|
||||
}
|
||||
|
||||
function onClipKeyDown(e) {
|
||||
if (!clipState.active) return;
|
||||
if (e.key === 'Escape') {
|
||||
cancelClip();
|
||||
} else if (e.key === 'Enter') {
|
||||
confirmClip();
|
||||
}
|
||||
}
|
||||
|
||||
function confirmClip() {
|
||||
if (!clipState.rect) return;
|
||||
|
||||
var rectLeft = parseInt(clipState.rect.style.left);
|
||||
var rectTop = parseInt(clipState.rect.style.top);
|
||||
var rectW = parseInt(clipState.rect.style.width);
|
||||
var rectH = parseInt(clipState.rect.style.height);
|
||||
|
||||
if (isNaN(rectW) || isNaN(rectH) || rectW < 10 || rectH < 10) {
|
||||
showNotification('选区太小,请重新框选', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 先提取选区内的文字内容
|
||||
var content = extractTextInRect(
|
||||
rectLeft + window.scrollX,
|
||||
rectTop + window.scrollY,
|
||||
rectW,
|
||||
rectH
|
||||
);
|
||||
|
||||
// 2. 保存裁剪参数(异步回调中使用)
|
||||
var cropLeft = rectLeft;
|
||||
var cropTop = rectTop;
|
||||
var cropW = rectW;
|
||||
var cropH = rectH;
|
||||
|
||||
// 3. 逐个隐藏截图工具元素,确保截图干净
|
||||
// 先隐藏选区框(绿色边框)、resize handles(小方块)、工具栏
|
||||
if (clipState.rect) clipState.rect.style.display = 'none';
|
||||
if (clipState.toolbar) clipState.toolbar.style.display = 'none';
|
||||
// 隐藏所有 handles(以防有残留的)
|
||||
if (clipState.overlay) {
|
||||
var handles = clipState.overlay.querySelectorAll('.ka-clip-handle');
|
||||
handles.forEach(function (h) { h.style.display = 'none'; });
|
||||
// 隐藏提示文字
|
||||
var tip = clipState.overlay.querySelector('.ka-clip-tip');
|
||||
if (tip) tip.style.display = 'none';
|
||||
}
|
||||
// 最后隐藏整个 overlay(包括暗幕遮罩)
|
||||
if (clipState.overlay) clipState.overlay.style.display = 'none';
|
||||
|
||||
// 4. 双重 requestAnimationFrame 确保浏览器完成渲染后再截图
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function () {
|
||||
chrome.runtime.sendMessage({ type: 'CAPTURE_SCREENSHOT' }, function (resp) {
|
||||
// 检查连接错误
|
||||
var lastErr = chrome.runtime.lastError;
|
||||
|
||||
// 立即清除遮罩
|
||||
cancelClip();
|
||||
|
||||
if (lastErr || !resp || !resp.success || !resp.dataUrl) {
|
||||
// 截图失败,回退到纯文字保存
|
||||
doSaveSelectClip(content, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 用 canvas 裁剪选区部分
|
||||
var img = new Image();
|
||||
img.onload = function () {
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = cropW * dpr;
|
||||
canvas.height = cropH * dpr;
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img,
|
||||
cropLeft * dpr, cropTop * dpr, cropW * dpr, cropH * dpr,
|
||||
0, 0, cropW * dpr, cropH * dpr
|
||||
);
|
||||
var croppedDataUrl = canvas.toDataURL('image/jpeg', 0.8);
|
||||
doSaveSelectClip(content, croppedDataUrl);
|
||||
};
|
||||
img.onerror = function () {
|
||||
doSaveSelectClip(content, null);
|
||||
};
|
||||
img.src = resp.dataUrl;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 保存选择剪藏数据(独立函数,不依赖 clipState)
|
||||
function doSaveSelectClip(textContent, screenshotDataUrl) {
|
||||
// 只要有截图或有文字,就保存
|
||||
var hasText = textContent && textContent.trim().length > 0;
|
||||
var hasScreenshot = !!screenshotDataUrl;
|
||||
|
||||
if (!hasText && !hasScreenshot) {
|
||||
showNotification('选区内没有可保存的内容', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var title = document.title || location.hostname;
|
||||
var clipData = {
|
||||
type: 'select-clip',
|
||||
content: hasText ? textContent.trim() : '(截图内容)',
|
||||
title: '截图 - ' + title,
|
||||
meta: { url: location.href, title: title, timestamp: Date.now() }
|
||||
};
|
||||
|
||||
if (hasScreenshot) {
|
||||
clipData.screenshot = screenshotDataUrl;
|
||||
}
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'SAVE_CLIP',
|
||||
payload: clipData
|
||||
}, function (resp) {
|
||||
var err = chrome.runtime.lastError;
|
||||
if (err) {
|
||||
showNotification('保存失败: ' + err.message, 'error');
|
||||
return;
|
||||
}
|
||||
if (resp && resp.success) {
|
||||
showNotification('内容已截取保存', 'success');
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'CLIP_RESULT',
|
||||
payload: { title: clipData.title, content: clipData.content }
|
||||
});
|
||||
} else {
|
||||
showNotification('保存失败: ' + ((resp && resp.error) || '未知错误'), 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function extractTextInRect(absX, absY, w, h) {
|
||||
// 隐藏遮罩以免干扰元素查找
|
||||
if (clipState.overlay) clipState.overlay.style.display = 'none';
|
||||
|
||||
var texts = [];
|
||||
var walker = document.createTreeWalker(
|
||||
document.body,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
var node;
|
||||
while (node = walker.nextNode()) {
|
||||
var text = node.textContent.trim();
|
||||
if (!text) continue;
|
||||
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
var rects = range.getClientRects();
|
||||
|
||||
for (var i = 0; i < rects.length; i++) {
|
||||
var r = rects[i];
|
||||
var nodeAbsX = r.left + window.scrollX;
|
||||
var nodeAbsY = r.top + window.scrollY;
|
||||
|
||||
// 检查是否在选区内(有交集)
|
||||
if (nodeAbsX + r.width > absX &&
|
||||
nodeAbsX < absX + w &&
|
||||
nodeAbsY + r.height > absY &&
|
||||
nodeAbsY < absY + h) {
|
||||
texts.push(text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clipState.overlay) clipState.overlay.style.display = '';
|
||||
|
||||
// 去重并保持顺序
|
||||
var seen = {};
|
||||
var unique = [];
|
||||
texts.forEach(function (t) {
|
||||
if (!seen[t]) {
|
||||
seen[t] = true;
|
||||
unique.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
return unique.join('\n');
|
||||
}
|
||||
|
||||
function cancelClip() {
|
||||
clipState.active = false;
|
||||
document.removeEventListener('keydown', onClipKeyDown);
|
||||
document.removeEventListener('mousemove', onClipMouseMove);
|
||||
document.removeEventListener('mouseup', onClipMouseUp);
|
||||
if (clipState.overlay) {
|
||||
clipState.overlay.remove();
|
||||
clipState.overlay = null;
|
||||
}
|
||||
clipState.rect = null;
|
||||
clipState.toolbar = null;
|
||||
clipState.dimTop = null;
|
||||
clipState.dimBottom = null;
|
||||
clipState.dimLeft = null;
|
||||
clipState.dimRight = null;
|
||||
}
|
||||
|
||||
// === 页面内通知 ===
|
||||
function showNotification(msg, type) {
|
||||
var n = document.createElement('div');
|
||||
n.className = 'ka-notification ka-notification-' + (type || 'info');
|
||||
n.textContent = msg;
|
||||
document.body.appendChild(n);
|
||||
|
||||
requestAnimationFrame(function () {
|
||||
n.classList.add('ka-notification-show');
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
n.classList.remove('ka-notification-show');
|
||||
setTimeout(function () { n.remove(); }, 300);
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
// === 消息监听 ===
|
||||
function ensureMessageListener() {
|
||||
// 移除旧版监听器(如果有)
|
||||
if (window.__kaMessageListener) {
|
||||
try { chrome.runtime.onMessage.removeListener(window.__kaMessageListener); } catch (e) {}
|
||||
}
|
||||
|
||||
window.__kaMessageListener = function (msg, sender, sendResponse) {
|
||||
if (msg.type === 'SMART_CLIP') {
|
||||
smartClip();
|
||||
sendResponse({ success: true });
|
||||
}
|
||||
if (msg.type === 'SELECT_CLIP') {
|
||||
startSelectClip();
|
||||
sendResponse({ success: true });
|
||||
}
|
||||
if (msg.type === 'SHOW_NOTIFICATION' && msg.payload) {
|
||||
showNotification(msg.payload.msg, msg.payload.status);
|
||||
sendResponse({ success: true });
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
chrome.runtime.onMessage.addListener(window.__kaMessageListener);
|
||||
}
|
||||
|
||||
// === 选中文字气泡 ===
|
||||
var selBubble = null;
|
||||
var selBubbleHideTimer = null;
|
||||
|
||||
function removeSelBubble() {
|
||||
if (selBubble) {
|
||||
selBubble.remove();
|
||||
selBubble = null;
|
||||
}
|
||||
if (selBubbleHideTimer) {
|
||||
clearTimeout(selBubbleHideTimer);
|
||||
selBubbleHideTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function createSelBubble(text, rect) {
|
||||
removeSelBubble();
|
||||
|
||||
var bubble = document.createElement('div');
|
||||
bubble.className = 'ka-sel-bubble';
|
||||
|
||||
// 保存到笔记按钮
|
||||
var saveBtn = document.createElement('button');
|
||||
saveBtn.className = 'ka-sel-btn ka-sel-btn-save';
|
||||
saveBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>保存到笔记';
|
||||
|
||||
// 分隔线
|
||||
var divider = document.createElement('div');
|
||||
divider.className = 'ka-sel-divider';
|
||||
|
||||
// 问 WeKnora 按钮
|
||||
var askBtn = document.createElement('button');
|
||||
askBtn.className = 'ka-sel-btn ka-sel-btn-ask';
|
||||
askBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>问 WeKnora';
|
||||
|
||||
bubble.appendChild(saveBtn);
|
||||
bubble.appendChild(divider);
|
||||
bubble.appendChild(askBtn);
|
||||
|
||||
// 小三角
|
||||
var arrow = document.createElement('div');
|
||||
arrow.className = 'ka-sel-arrow';
|
||||
bubble.appendChild(arrow);
|
||||
|
||||
document.body.appendChild(bubble);
|
||||
selBubble = bubble;
|
||||
|
||||
// 定位:在选区上方居中
|
||||
var bubbleW = bubble.offsetWidth;
|
||||
var bubbleH = bubble.offsetHeight;
|
||||
var left = rect.left + rect.width / 2 - bubbleW / 2 + window.scrollX;
|
||||
var top = rect.top - bubbleH - 10 + window.scrollY;
|
||||
|
||||
// 防止溢出左右
|
||||
if (left < 8) left = 8;
|
||||
if (left + bubbleW > document.documentElement.scrollWidth - 8) {
|
||||
left = document.documentElement.scrollWidth - bubbleW - 8;
|
||||
}
|
||||
// 如果上方空间不够,放到下方
|
||||
if (rect.top - bubbleH - 10 < 0) {
|
||||
top = rect.bottom + 10 + window.scrollY;
|
||||
arrow.className = 'ka-sel-arrow ka-sel-arrow-top';
|
||||
}
|
||||
|
||||
bubble.style.left = left + 'px';
|
||||
bubble.style.top = top + 'px';
|
||||
|
||||
// "问 WeKnora" — 发送到 sidepanel
|
||||
askBtn.addEventListener('click', function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'ASK_WEKNORA',
|
||||
payload: { text: text }
|
||||
}, function () {
|
||||
void chrome.runtime.lastError;
|
||||
});
|
||||
removeSelBubble();
|
||||
window.getSelection().removeAllRanges();
|
||||
});
|
||||
|
||||
// "保存到笔记" — 保存到 ka_clips
|
||||
saveBtn.addEventListener('click', function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
var clipTitle = '选中文本 - ' + (document.title || location.hostname);
|
||||
var clipPayload = {
|
||||
type: 'select-clip',
|
||||
content: text,
|
||||
title: clipTitle,
|
||||
meta: { url: location.href, title: document.title || '' }
|
||||
};
|
||||
|
||||
function doSave(retryCount) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'SAVE_SELECTION',
|
||||
payload: clipPayload
|
||||
}, function (saveResp) {
|
||||
var err = chrome.runtime.lastError;
|
||||
if (err) {
|
||||
// service worker 可能休眠,重试一次
|
||||
if (retryCount < 1) {
|
||||
setTimeout(function () { doSave(retryCount + 1); }, 500);
|
||||
} else {
|
||||
showNotification('保存失败: ' + err.message, 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (saveResp && saveResp.success) {
|
||||
showNotification('已保存到笔记', 'success');
|
||||
} else {
|
||||
showNotification('保存失败: ' + ((saveResp && saveResp.error) || '未知错误'), 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
doSave(0);
|
||||
removeSelBubble();
|
||||
window.getSelection().removeAllRanges();
|
||||
});
|
||||
|
||||
// 点击气泡外关闭
|
||||
setTimeout(function () {
|
||||
document.addEventListener('mousedown', onBubbleOutsideClick);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function onBubbleOutsideClick(e) {
|
||||
if (selBubble && !selBubble.contains(e.target)) {
|
||||
removeSelBubble();
|
||||
document.removeEventListener('mousedown', onBubbleOutsideClick);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听鼠标抬起 → 检测选中文字
|
||||
document.addEventListener('mouseup', function (e) {
|
||||
// 如果正在框选剪藏或点击在气泡内,不触发
|
||||
if (clipState.active) return;
|
||||
if (selBubble && selBubble.contains(e.target)) return;
|
||||
if (imgBubble && imgBubble.contains(e.target)) return;
|
||||
|
||||
// 延迟一点让 selection 更新
|
||||
setTimeout(function () {
|
||||
var sel = window.getSelection();
|
||||
var text = sel ? sel.toString().trim() : '';
|
||||
|
||||
if (text.length < 2) {
|
||||
// 没有选中有效文字,移除气泡
|
||||
removeSelBubble();
|
||||
document.removeEventListener('mousedown', onBubbleOutsideClick);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取选区矩形
|
||||
if (sel.rangeCount > 0) {
|
||||
var range = sel.getRangeAt(0);
|
||||
var rects = range.getClientRects();
|
||||
if (rects.length > 0) {
|
||||
// 使用第一个 rect 的顶部和整体范围来定位
|
||||
var boundRect = range.getBoundingClientRect();
|
||||
createSelBubble(text, boundRect);
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
|
||||
// === 右键图片气泡 ===
|
||||
var imgBubble = null;
|
||||
|
||||
function removeImgBubble() {
|
||||
if (imgBubble) {
|
||||
imgBubble.remove();
|
||||
imgBubble = null;
|
||||
}
|
||||
}
|
||||
|
||||
function createImgBubble(imgSrc, imgAlt, rect) {
|
||||
removeImgBubble();
|
||||
removeSelBubble(); // 同时移除文字气泡
|
||||
|
||||
var bubble = document.createElement('div');
|
||||
bubble.className = 'ka-sel-bubble ka-img-bubble';
|
||||
|
||||
// 保存图片按钮
|
||||
var saveBtn = document.createElement('button');
|
||||
saveBtn.className = 'ka-sel-btn ka-sel-btn-save';
|
||||
saveBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>保存图片到笔记';
|
||||
|
||||
// 分隔线
|
||||
var divider = document.createElement('div');
|
||||
divider.className = 'ka-sel-divider';
|
||||
|
||||
// 问 WeKnora 按钮
|
||||
var askBtn = document.createElement('button');
|
||||
askBtn.className = 'ka-sel-btn ka-sel-btn-ask';
|
||||
askBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>问 WeKnora';
|
||||
|
||||
bubble.appendChild(saveBtn);
|
||||
bubble.appendChild(divider);
|
||||
bubble.appendChild(askBtn);
|
||||
|
||||
// 小三角
|
||||
var arrow = document.createElement('div');
|
||||
arrow.className = 'ka-sel-arrow';
|
||||
bubble.appendChild(arrow);
|
||||
|
||||
document.body.appendChild(bubble);
|
||||
imgBubble = bubble;
|
||||
|
||||
// 定位:在图片上方居中
|
||||
var bubbleW = bubble.offsetWidth;
|
||||
var bubbleH = bubble.offsetHeight;
|
||||
var left = rect.left + rect.width / 2 - bubbleW / 2 + window.scrollX;
|
||||
var top = rect.top - bubbleH - 10 + window.scrollY;
|
||||
|
||||
// 防止溢出左右
|
||||
if (left < 8) left = 8;
|
||||
if (left + bubbleW > document.documentElement.scrollWidth - 8) {
|
||||
left = document.documentElement.scrollWidth - bubbleW - 8;
|
||||
}
|
||||
// 如果上方空间不够,放到下方
|
||||
if (rect.top - bubbleH - 10 < 0) {
|
||||
top = rect.bottom + 10 + window.scrollY;
|
||||
arrow.className = 'ka-sel-arrow ka-sel-arrow-top';
|
||||
}
|
||||
|
||||
bubble.style.left = left + 'px';
|
||||
bubble.style.top = top + 'px';
|
||||
|
||||
// "保存图片到笔记"
|
||||
saveBtn.addEventListener('click', function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
var clipTitle = '图片收藏 - ' + (document.title || location.hostname);
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'SAVE_IMAGE',
|
||||
payload: {
|
||||
type: 'image-clip',
|
||||
content: '',
|
||||
title: clipTitle,
|
||||
meta: { url: location.href, title: document.title || '', imageUrl: imgSrc }
|
||||
}
|
||||
}, function (saveResp) {
|
||||
void chrome.runtime.lastError;
|
||||
if (saveResp && saveResp.success) {
|
||||
showNotification('图片已保存到笔记', 'success');
|
||||
} else {
|
||||
showNotification('保存失败', 'error');
|
||||
}
|
||||
});
|
||||
removeImgBubble();
|
||||
});
|
||||
|
||||
// "问 WeKnora" — 发送图片描述到 sidepanel
|
||||
askBtn.addEventListener('click', function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
var question = '请描述这张图片的内容: ' + imgSrc;
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'ASK_WEKNORA',
|
||||
payload: { text: question }
|
||||
}, function () {
|
||||
void chrome.runtime.lastError;
|
||||
});
|
||||
removeImgBubble();
|
||||
});
|
||||
|
||||
// 点击气泡外关闭
|
||||
setTimeout(function () {
|
||||
document.addEventListener('mousedown', onImgBubbleOutsideClick);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function onImgBubbleOutsideClick(e) {
|
||||
if (imgBubble && !imgBubble.contains(e.target)) {
|
||||
removeImgBubble();
|
||||
document.removeEventListener('mousedown', onImgBubbleOutsideClick);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听右键点击图片 → 弹出气泡
|
||||
document.addEventListener('contextmenu', function (e) {
|
||||
// 如果正在框选剪藏,不触发
|
||||
if (clipState.active) return;
|
||||
|
||||
var target = e.target;
|
||||
// 检查是否右键点击了图片
|
||||
if (target.tagName === 'IMG' && target.src) {
|
||||
// 延迟一点,让浏览器原生右键菜单先出现
|
||||
setTimeout(function () {
|
||||
var imgRect = target.getBoundingClientRect();
|
||||
createImgBubble(target.src, target.alt || '', imgRect);
|
||||
}, 100);
|
||||
} else {
|
||||
// 不是图片,移除图片气泡
|
||||
removeImgBubble();
|
||||
}
|
||||
});
|
||||
|
||||
// === Option + Command 快捷键直接触发选择剪藏 ===
|
||||
// Chrome 扩展 commands API 不支持 Alt+Command 组合,所以在页面层监听
|
||||
var optCmdState = { altDown: false, metaDown: false };
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
// 如果正在剪藏中,不响应
|
||||
if (clipState.active) return;
|
||||
|
||||
// 记录按键状态
|
||||
if (e.key === 'Alt') optCmdState.altDown = true;
|
||||
if (e.key === 'Meta') optCmdState.metaDown = true;
|
||||
|
||||
// 同时按住 Option(Alt) + Command(Meta) 触发选择剪藏
|
||||
if (optCmdState.altDown && optCmdState.metaDown) {
|
||||
e.preventDefault();
|
||||
optCmdState.altDown = false;
|
||||
optCmdState.metaDown = false;
|
||||
startSelectClip();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', function (e) {
|
||||
if (e.key === 'Alt') optCmdState.altDown = false;
|
||||
if (e.key === 'Meta') optCmdState.metaDown = false;
|
||||
});
|
||||
|
||||
// 窗口失焦时重置状态,防止按键卡住
|
||||
window.addEventListener('blur', function () {
|
||||
optCmdState.altDown = false;
|
||||
optCmdState.metaDown = false;
|
||||
});
|
||||
|
||||
// 暴露到 window 上以便防重复注入时调用
|
||||
window.ensureMessageListener = ensureMessageListener;
|
||||
|
||||
ensureMessageListener();
|
||||
})();
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 799 B |
Binary file not shown.
|
Before Width: | Height: | Size: 160 B |
Binary file not shown.
|
Before Width: | Height: | Size: 234 B |
Binary file not shown.
|
Before Width: | Height: | Size: 317 B |
@@ -1,76 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "快速笔记 5.0",
|
||||
"version": "5.0.0",
|
||||
"description": "快速笔记 5.0 - 知识库问答、网页内容采集、Markdown 速记",
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"default_title": "快速笔记 5.0"
|
||||
},
|
||||
"side_panel": {
|
||||
"default_path": "sidepanel.html"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content.js"],
|
||||
"css": ["content.css"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab",
|
||||
"sidePanel",
|
||||
"scripting",
|
||||
"contextMenus",
|
||||
"tabs"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["weknora.html", "weknora.js", "clips.html", "clips.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"commands": {
|
||||
"open-sidepanel": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+K",
|
||||
"mac": "Command+Shift+K"
|
||||
},
|
||||
"description": "打开侧边栏"
|
||||
},
|
||||
"quick-ask": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+A",
|
||||
"mac": "Command+Shift+A"
|
||||
},
|
||||
"description": "快速提问"
|
||||
},
|
||||
"select-clip": {
|
||||
"suggested_key": {
|
||||
"default": "Ctrl+Shift+S",
|
||||
"mac": "Command+Shift+S"
|
||||
},
|
||||
"description": "选择剪藏"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,978 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>快速笔记 5.0</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root { --primary: #07C160; --primary-dark: #06ad54; --bg: #f5f6f7; --white: #fff; --text: #333; --text2: #666; --text3: #999; --border: #e5e5e5; --shadow: 0 2px 12px rgba(0,0,0,0.08); --radius: 12px; }
|
||||
body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--text); overflow: hidden; }
|
||||
|
||||
/* Pages */
|
||||
.page { display: none; flex-direction: column; overflow: hidden; }
|
||||
.page.active { display: flex; }
|
||||
|
||||
/* === Page 1: 选择登录 === */
|
||||
#pg-login { align-items: center; justify-content: center; background: linear-gradient(135deg, #07C160 0%, #06ad54 100%); color: #fff; flex: 1; min-height: 480px; }
|
||||
#pg-login .logo { width: 72px; height: 72px; background: rgba(255,255,255,0.2); border-radius: 18px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px; }
|
||||
#pg-login .logo img { width: 48px; height: 48px; }
|
||||
#pg-login h1 { font-size: 22px; font-weight: 600; margin-bottom: 6px; }
|
||||
#pg-login .subtitle { font-size: 13px; opacity: 0.85; margin-bottom: 40px; }
|
||||
.login-btn { width: 280px; height: 48px; border: none; border-radius: 24px; font-size: 15px; font-weight: 500; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s; margin-bottom: 14px; }
|
||||
.login-btn:active { transform: scale(0.97); }
|
||||
#btn-ka { background: #fff; color: var(--primary); }
|
||||
#btn-ka:hover { background: #f0faf4; }
|
||||
#btn-wk { background: rgba(255,255,255,0.18); color: #fff; border: 1.5px solid rgba(255,255,255,0.5); }
|
||||
#btn-wk:hover { background: rgba(255,255,255,0.28); }
|
||||
.login-btn svg { width: 20px; height: 20px; }
|
||||
|
||||
/* === Page 2: 快速笔记 5.0 登录(扫码 + API Key 双模式) === */
|
||||
#pg-ka-login { background: var(--white); min-height: 480px; }
|
||||
.ka-header { padding: 16px 20px 0; display: flex; align-items: center; gap: 10px; }
|
||||
.ka-back { width: 32px; height: 32px; border: none; background: #f5f5f5; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); transition: color 0.12s; }
|
||||
.ka-back:hover { color: var(--text); }
|
||||
.ka-back svg { width: 18px; height: 18px; }
|
||||
.ka-title { font-size: 15px; font-weight: 600; color: var(--text); }
|
||||
|
||||
/* 登录方式 Tab */
|
||||
.ka-tabs { display: flex; margin: 16px 20px 0; background: #f3f3f3; border-radius: 8px; padding: 3px; }
|
||||
.ka-tab { flex: 1; height: 34px; border: none; border-radius: 6px; background: transparent; font-size: 13px; font-weight: 500; color: var(--text3); cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 5px; transition: all 0.15s; }
|
||||
.ka-tab.active { background: #fff; color: var(--text); }
|
||||
.ka-tab svg { width: 14px; height: 14px; }
|
||||
|
||||
/* 登录内容区 */
|
||||
.ka-body { padding: 20px; }
|
||||
.ka-panel { display: none; }
|
||||
.ka-panel.active { display: flex; flex-direction: column; align-items: center; }
|
||||
|
||||
/* 扫码面板 */
|
||||
.qr-wrap { display: flex; flex-direction: column; align-items: center; padding: 6px 0; }
|
||||
.qr-box { width: 200px; height: 200px; background: #fff; border: 1px solid #eee; border-radius: 12px; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; margin-bottom: 14px; }
|
||||
.qr-box canvas { width: 180px; height: 180px; cursor: pointer; border-radius: 6px; }
|
||||
.qr-box canvas:active { opacity: 0.7; }
|
||||
.qr-box .qr-placeholder { width: 180px; height: 180px; background: #f8f8f8; border-radius: 6px; display: flex; align-items: center; justify-content: center; }
|
||||
.qr-tip { font-size: 13px; font-weight: 500; color: var(--text); margin-bottom: 4px; }
|
||||
.qr-sub { font-size: 12px; color: var(--text3); margin-bottom: 14px; }
|
||||
.qr-status { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text3); padding: 6px 14px; background: #f5f5f5; border-radius: 6px; }
|
||||
.qr-status .dot { width: 6px; height: 6px; border-radius: 50%; background: #ccc; animation: pulse-dot 1.2s ease infinite; }
|
||||
.qr-status.scanned { color: var(--primary); background: rgba(7,193,96,0.06); }
|
||||
.qr-status.scanned .dot { background: var(--primary); }
|
||||
@keyframes pulse-dot { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
|
||||
.qr-refresh { margin-top: 10px; border: none; background: none; color: var(--primary); font-size: 12px; cursor: pointer; padding: 4px 10px; border-radius: 6px; transition: background 0.12s; }
|
||||
.qr-refresh:hover { background: rgba(7,193,96,0.06); }
|
||||
|
||||
/* API Key 面板 */
|
||||
.ka-form { width: 100%; }
|
||||
.ka-form .form-group { margin-bottom: 14px; }
|
||||
.ka-form .form-group label { display: block; font-size: 13px; font-weight: 500; color: var(--text2); margin-bottom: 5px; }
|
||||
.ka-form .form-group input { width: 100%; height: 42px; border: 1px solid #eee; border-radius: 8px; padding: 0 12px; font-size: 13px; outline: none; transition: border-color 0.12s; }
|
||||
.ka-form .form-group input:focus { border-color: #ccc; }
|
||||
.ka-form .form-group input::placeholder { color: #ccc; }
|
||||
.ka-test-msg { font-size: 12px; text-align: center; min-height: 16px; margin-bottom: 8px; }
|
||||
.ka-test-msg.ok { color: var(--primary); }
|
||||
.ka-test-msg.err { color: #e44; }
|
||||
.ka-form .btn-primary { width: 100%; }
|
||||
|
||||
/* === Page 3: WeKnora 配置 === */
|
||||
#pg-weknora { background: var(--white); min-height: 480px; }
|
||||
.wk-header { padding: 24px 20px 16px; text-align: center; }
|
||||
.wk-header h2 { font-size: 18px; font-weight: 600; margin-bottom: 4px; }
|
||||
.wk-header p { font-size: 13px; color: var(--text3); }
|
||||
.wk-form { padding: 0 20px; flex: 1; }
|
||||
.form-group { margin-bottom: 18px; }
|
||||
.form-group label { display: block; font-size: 13px; font-weight: 500; color: var(--text2); margin-bottom: 6px; }
|
||||
.form-group input { width: 100%; height: 44px; border: 1.5px solid var(--border); border-radius: 10px; padding: 0 14px; font-size: 14px; outline: none; transition: border-color 0.2s; }
|
||||
.form-group input:focus { border-color: var(--primary); }
|
||||
.form-group input::placeholder { color: #ccc; }
|
||||
.wk-actions { padding: 0 20px 24px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.btn-primary { height: 46px; border: none; border-radius: 23px; background: var(--primary); color: #fff; font-size: 15px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
|
||||
.btn-primary:hover { background: var(--primary-dark); }
|
||||
.btn-primary:disabled { background: #ccc; cursor: not-allowed; }
|
||||
.btn-secondary { height: 40px; border: 1.5px solid var(--border); border-radius: 20px; background: transparent; color: var(--text2); font-size: 14px; cursor: pointer; transition: all 0.2s; }
|
||||
.btn-secondary:hover { border-color: var(--text3); }
|
||||
.wk-test-result { font-size: 12px; text-align: center; min-height: 20px; margin-top: 4px; }
|
||||
.wk-test-result.ok { color: var(--primary); }
|
||||
.wk-test-result.err { color: #e44; }
|
||||
.wk-help { display: flex; align-items: center; gap: 6px; padding: 10px 14px; font-size: 12px; color: #8a7340; background: #fefaf0; border-radius: 10px; margin-top: 4px; }
|
||||
.wk-help svg { width: 14px; height: 14px; flex-shrink: 0; color: #c9a23c; }
|
||||
.wk-help a { color: var(--primary); text-decoration: none; font-weight: 500; }
|
||||
.wk-help a:hover { text-decoration: underline; }
|
||||
|
||||
/* === Page 4: 主界面 === */
|
||||
#pg-main { background: #fff; overflow-y: auto; }
|
||||
|
||||
/* 顶栏 */
|
||||
.main-top { padding: 12px 18px; display: flex; align-items: center; gap: 12px; }
|
||||
.main-avatar { width: 38px; height: 38px; border-radius: 50%; overflow: hidden; flex-shrink: 0; }
|
||||
.main-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.main-avatar .letter-avatar { width: 100%; height: 100%; background: var(--primary); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 600; border-radius: 50%; }
|
||||
.main-info { flex: 1; min-width: 0; }
|
||||
.main-name { font-size: 14px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.main-badge { display: inline-block; font-size: 10px; padding: 2px 8px; border-radius: 6px; margin-top: 2px; font-weight: 500; }
|
||||
.badge-ka { background: rgba(7,193,96,0.12); color: var(--primary); }
|
||||
.badge-wk { background: rgba(7,193,96,0.12); color: var(--primary); }
|
||||
.main-actions { position: relative; }
|
||||
.more-btn { width: 30px; height: 30px; border: none; background: transparent; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); transition: color 0.15s; }
|
||||
.more-btn:hover { color: var(--text); }
|
||||
.more-btn svg { width: 20px; height: 20px; }
|
||||
.more-menu { position: absolute; top: 100%; right: 0; margin-top: 4px; background: var(--white); border-radius: 10px; box-shadow: 0 6px 24px rgba(0,0,0,0.1); padding: 4px 0; min-width: 140px; display: none; z-index: 50; }
|
||||
.more-menu.show { display: block; }
|
||||
.more-menu-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; font-size: 13px; color: var(--text2); cursor: pointer; transition: all 0.12s; border: none; background: none; width: 100%; text-align: left; }
|
||||
.more-menu-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
|
||||
.more-menu-item svg { width: 16px; height: 16px; flex-shrink: 0; }
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content { flex: 1; display: flex; flex-direction: column; padding: 14px 18px 14px; overflow-y: auto; }
|
||||
|
||||
/* ===== 知识采集 — 卡片 + 浮动胶囊 ===== */
|
||||
.collect-wrap {
|
||||
position: relative;
|
||||
padding-bottom: 30px; /* 胶囊按钮露出空间 */
|
||||
margin-bottom: 6px;
|
||||
perspective: 1000px;
|
||||
}
|
||||
/* 翻转时隐藏胶囊 */
|
||||
.collect-wrap.flipped .collect-fab {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%) scale(0.8);
|
||||
}
|
||||
/* 翻转时不需要底部占位 */
|
||||
.collect-wrap.flipped {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.collect-card-flipper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 220px;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
.collect-wrap.flipped .collect-card-flipper {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.collect-card-front,
|
||||
.collect-card-back {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.06), 0 4px 24px rgba(0, 0, 0, 0.10);
|
||||
background: var(--white);
|
||||
overflow: hidden;
|
||||
min-height: 220px;
|
||||
}
|
||||
.collect-card-front {
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.25s ease;
|
||||
}
|
||||
.collect-card-front:hover {
|
||||
box-shadow: 0 0 12px rgba(0, 0, 0, 0.08), 0 8px 32px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
.collect-card-back {
|
||||
transform: rotateY(180deg);
|
||||
z-index: 1;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%; /* 填满 flipper 高度 */
|
||||
}
|
||||
/* flipper 高度由可见面的内容驱动 */
|
||||
.collect-card-flipper { min-height: 220px; }
|
||||
|
||||
/* 空状态 */
|
||||
.collect-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 60px 16px 70px;
|
||||
color: #b5c9bc;
|
||||
}
|
||||
.collect-empty svg { width: 30px; height: 30px; opacity: 0.4; }
|
||||
.collect-empty span { font-size: 13px; font-weight: 500; color: #aebfb4; }
|
||||
.collect-empty-sub { font-size: 11px; font-weight: 400; color: #c8d5cc; }
|
||||
|
||||
/* 有内容 */
|
||||
.collect-content { padding: 14px 16px 16px; }
|
||||
.collect-content-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.collect-content-meta { display: flex; align-items: center; gap: 8px; }
|
||||
.latest-clip-tag {
|
||||
font-size: 10px;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
background: rgba(7, 193, 96, 0.12);
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.latest-clip-tag.tag-markdown { background: rgba(59,125,216,0.12); color: #3b7dd8; }
|
||||
.latest-clip-tag.tag-image { background: rgba(233,89,80,0.12); color: #e95950; }
|
||||
.latest-clip-tag.tag-text { background: rgba(134,142,150,0.12); color: #868e96; }
|
||||
.latest-clip-time { font-size: 10px; color: #bbb; }
|
||||
|
||||
.collect-content-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
border: none;
|
||||
background: #f5f6f7;
|
||||
font-size: 11px;
|
||||
color: var(--text3);
|
||||
cursor: pointer;
|
||||
padding: 3px 8px 3px 10px;
|
||||
border-radius: 10px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.collect-content-more:hover { color: var(--primary); background: rgba(7,193,96,0.08); }
|
||||
.collect-content-more svg { width: 12px; height: 12px; }
|
||||
|
||||
.collect-content-body {
|
||||
font-size: 13px;
|
||||
color: var(--text2);
|
||||
line-height: 1.65;
|
||||
max-height: 130px;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
/* 只在内容溢出时才显示底部渐隐 */
|
||||
.collect-content-body.overflowing {
|
||||
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 88%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, #000 0%, #000 88%, transparent 100%);
|
||||
}
|
||||
.collect-content-body img {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
max-height: 110px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ===== 浮动椭圆胶囊按钮 ===== */
|
||||
.collect-fab {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 5px 6px;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
z-index: 5;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
.collect-fab-btn {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(245, 246, 247, 0.85);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.18s ease;
|
||||
color: #666;
|
||||
}
|
||||
.collect-fab-btn svg { width: 17px; height: 17px; fill: currentColor; stroke: none; }
|
||||
.collect-fab-btn:hover {
|
||||
background: rgba(7, 193, 96, 0.1);
|
||||
color: var(--primary);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
.collect-fab-btn:active { transform: scale(0.94); }
|
||||
/* 中间主按钮高亮 */
|
||||
.collect-fab-btn.fab-primary {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
box-shadow: 0 3px 12px rgba(7, 193, 96, 0.3);
|
||||
}
|
||||
.collect-fab-btn.fab-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 16px rgba(7, 193, 96, 0.4);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
.collect-fab-btn.fab-primary svg { width: 19px; height: 19px; }
|
||||
|
||||
/* Markdown 速记 — 卡片背面 */
|
||||
.note-back-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.note-back-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.note-back-title svg { width: 13px; height: 13px; }
|
||||
.note-back-close {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: none;
|
||||
background: #f5f6f7;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text3);
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.note-back-close:hover { background: #eee; color: var(--text); }
|
||||
.note-back-close svg { width: 14px; height: 14px; }
|
||||
.note-card { background: transparent; border-radius: 0; overflow: hidden; border: none; flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
||||
.note-input { width: 100%; border: none; background: transparent; padding: 10px 14px; font-size: 13px; font-family: "SF Mono", "Fira Code", "Menlo", monospace; resize: none; outline: none; line-height: 1.6; color: var(--text); overflow-y: auto; flex: 1; min-height: 0; }
|
||||
.note-input::placeholder { color: #c0c0c0; }
|
||||
.note-preview { display: none; padding: 12px 14px; font-size: 13px; line-height: 1.7; color: var(--text2); overflow-y: auto; flex: 1; min-height: 0; }
|
||||
.note-preview h1 { font-size: 18px; font-weight: 700; margin: 0 0 8px; color: var(--text); border-bottom: 1px solid var(--border); padding-bottom: 5px; }
|
||||
.note-preview h2 { font-size: 16px; font-weight: 700; margin: 10px 0 6px; color: var(--text); }
|
||||
.note-preview h3 { font-size: 14px; font-weight: 600; margin: 8px 0 4px; color: var(--text); }
|
||||
.note-preview h4 { font-size: 13px; font-weight: 600; margin: 6px 0 3px; color: var(--text); }
|
||||
.note-preview p { margin: 4px 0; line-height: 1.7; }
|
||||
.note-preview ul, .note-preview ol { margin: 4px 0; padding-left: 18px; }
|
||||
.note-preview li { margin: 2px 0; line-height: 1.6; }
|
||||
.note-preview code { font-family: "SF Mono","Fira Code","Menlo",monospace; background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; color: #d63384; }
|
||||
.note-preview pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 10px 12px; overflow-x: auto; margin: 6px 0; font-size: 12px; line-height: 1.5; }
|
||||
.note-preview pre code { background: none; padding: 0; color: inherit; font-size: inherit; }
|
||||
.note-preview blockquote { border-left: 3px solid var(--primary); padding: 4px 12px; margin: 6px 0; background: #f8faf8; color: var(--text2); font-size: 13px; border-radius: 0 6px 6px 0; }
|
||||
.note-preview hr { border: none; border-top: 1px solid var(--border); margin: 8px 0; }
|
||||
.note-preview a { color: var(--primary); text-decoration: none; }
|
||||
.note-preview a:hover { text-decoration: underline; }
|
||||
.note-preview strong { font-weight: 700; }
|
||||
.note-preview em { font-style: italic; }
|
||||
.note-preview table { width: 100%; border-collapse: collapse; margin: 6px 0; font-size: 12px; }
|
||||
.note-preview th, .note-preview td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; }
|
||||
.note-preview th { background: #f5f6f7; font-weight: 600; }
|
||||
.note-preview img { max-width: 100%; border-radius: 6px; margin: 4px 0; }
|
||||
.note-tool-btn.active { background: rgba(7,193,96,0.12); color: var(--primary); }
|
||||
.note-toolbar { display: flex; align-items: center; padding: 2px 8px; gap: 0; border-top: 1px solid #edf8f1; flex-shrink: 0; }
|
||||
.note-toolbar-group { display: flex; align-items: center; gap: 0; }
|
||||
.note-toolbar-divider { width: 1px; height: 14px; background: #e0e8e2; margin: 0 6px; flex-shrink: 0; }
|
||||
.note-tool-btn { width: 28px; height: 28px; border: none; background: transparent; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #999; transition: all 0.15s; padding: 0; }
|
||||
.note-tool-btn:hover { background: rgba(7,193,96,0.08); color: #555; }
|
||||
.note-tool-btn:active { background: rgba(7,193,96,0.15); }
|
||||
.note-tool-btn svg { width: 15px; height: 15px; }
|
||||
.note-toolbar-spacer { flex: 1; }
|
||||
.note-bottom { display: flex; align-items: center; justify-content: flex-end; padding: 4px 10px 8px; }
|
||||
.note-save { height: 28px; width: 28px; border: none; border-radius: 50%; background: #ddd; color: #fff; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; }
|
||||
.note-save.active { background: var(--primary); }
|
||||
.note-save:hover.active { background: var(--primary-dark); box-shadow: 0 2px 8px rgba(7,193,96,0.25); }
|
||||
.note-save:disabled { background: #ddd; box-shadow: none; cursor: not-allowed; }
|
||||
.note-save svg { width: 14px; height: 14px; }
|
||||
|
||||
/* ===== 知识问答 — 单行输入栏 ===== */
|
||||
/* 底部输入区域整体包裹 */
|
||||
.chat-bar-wrapper {
|
||||
background: var(--white);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06), 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
margin-top: 4px;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.chat-bar-wrapper:focus-within {
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08), 0 0 0 1.5px rgba(7, 193, 96, 0.15);
|
||||
}
|
||||
|
||||
/* 顶部工具栏:知识库 + 模式选择 */
|
||||
.chat-bar-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 8px 0;
|
||||
}
|
||||
|
||||
.chat-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 5px 5px 8px;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
}
|
||||
/* 知识库选择按钮 */
|
||||
.chat-bar-kb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
border: none;
|
||||
background: #f3f4f5;
|
||||
border-radius: 50px;
|
||||
padding: 6px 8px 6px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.chat-bar-kb:hover { background: #eaebed; color: var(--text); }
|
||||
.chat-bar-kb svg { width: 13px; height: 13px; flex-shrink: 0; }
|
||||
.chat-bar-arrow { width: 10px !important; height: 10px !important; opacity: 0.5; }
|
||||
|
||||
/* 模式选择按钮 */
|
||||
.chat-bar-mode {
|
||||
display: flex; align-items: center; gap: 3px;
|
||||
border: none; background: #f3f4f5; border-radius: 50px;
|
||||
padding: 6px 8px 6px 10px; font-size: 11px; font-weight: 500;
|
||||
color: var(--text2); cursor: pointer; white-space: nowrap;
|
||||
transition: all 0.15s; flex-shrink: 0;
|
||||
}
|
||||
.chat-bar-mode:hover { background: #eaebed; color: var(--text); }
|
||||
.chat-bar-mode svg { width: 13px; height: 13px; flex-shrink: 0; }
|
||||
|
||||
/* 模式下拉菜单 */
|
||||
.popup-mode-menu { position: fixed; background: var(--white); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.12), 0 0 1px rgba(0,0,0,0.06); padding: 6px 0; min-width: 160px; display: none; z-index: 100; }
|
||||
.popup-mode-menu.show { display: block; }
|
||||
|
||||
/* 分隔竖线 */
|
||||
.chat-bar-divider { width: 1px; height: 16px; background: #e5e5e5; margin: 0 8px; flex-shrink: 0; }
|
||||
|
||||
/* 输入框 */
|
||||
.chat-bar-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
color: var(--text);
|
||||
padding: 6px 4px;
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
max-height: 100px;
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
line-height: 18px;
|
||||
}
|
||||
.chat-bar-input::placeholder { color: #c0c0c0; }
|
||||
|
||||
/* 发送按钮 */
|
||||
.chat-bar-send {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: #e8e8e8;
|
||||
color: #aaa;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.chat-bar-send svg { width: 14px; height: 14px; }
|
||||
.chat-bar-send.active { background: var(--primary); color: #fff; }
|
||||
.chat-bar-send.active:hover { background: var(--primary-dark); }
|
||||
.chat-send-btn { width: 30px; height: 30px; border: none; border-radius: 50%; background: #ddd; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.25s; flex-shrink: 0; }
|
||||
.chat-send-btn.active { background: var(--primary); }
|
||||
.chat-send-btn:hover.active { background: var(--primary-dark); }
|
||||
.chat-send-btn svg { width: 14px; height: 14px; }
|
||||
|
||||
/* 知识库合并下拉菜单 */
|
||||
.kb-dropdown { position: relative; display: inline-block; }
|
||||
.kb-dropdown-menu { position: fixed; background: var(--white); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.12), 0 0 1px rgba(0,0,0,0.06); padding: 6px 0; min-width: 200px; display: none; z-index: 100; max-height: 70vh; overflow-y: auto; }
|
||||
.kb-dropdown-menu.show { display: block; }
|
||||
.kb-dropdown-item { padding: 9px 14px; font-size: 13px; color: var(--text); cursor: pointer; display: flex; align-items: center; gap: 8px; transition: all 0.15s; }
|
||||
.kb-dropdown-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
|
||||
.kb-dropdown-item.selected { color: var(--primary); font-weight: 500; }
|
||||
.kb-dropdown-item .kb-radio { width: 16px; height: 16px; border: 2px solid #ccc; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: all 0.15s; }
|
||||
.kb-dropdown-item.selected .kb-radio { border-color: var(--primary); }
|
||||
.kb-dropdown-item.selected .kb-radio::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--primary); }
|
||||
.kb-dropdown-item:hover .kb-radio { border-color: var(--primary); }
|
||||
.kb-dropdown-item svg { width: 14px; height: 14px; }
|
||||
|
||||
/* 下拉菜单分隔线和分组标题 */
|
||||
.kb-dropdown-divider { height: 1px; background: #f0f0f0; margin: 4px 12px; }
|
||||
.kb-dropdown-section { padding: 6px 14px 2px; font-size: 10px; font-weight: 600; color: #bbb; letter-spacing: 0.5px; text-transform: uppercase; }
|
||||
|
||||
/* 模式选项 */
|
||||
.kb-mode-item { padding: 8px 14px; font-size: 13px; color: var(--text); cursor: pointer; display: flex; align-items: center; gap: 8px; transition: all 0.15s; }
|
||||
.kb-mode-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
|
||||
.kb-mode-item.selected { color: var(--primary); font-weight: 500; }
|
||||
.kb-mode-item .kb-radio { width: 14px; height: 14px; border: 2px solid #ccc; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: all 0.15s; }
|
||||
.kb-mode-item.selected .kb-radio { border-color: var(--primary); }
|
||||
.kb-mode-item.selected .kb-radio::after { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--primary); }
|
||||
.kb-mode-item:hover .kb-radio { border-color: var(--primary); }
|
||||
.kb-mode-item svg { width: 14px; height: 14px; }
|
||||
|
||||
/* 附件选项 */
|
||||
/* 图片上传按钮 */
|
||||
.popup-image-btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: none; background: none; color: #999; cursor: pointer;
|
||||
width: 28px; height: 28px; padding: 0; flex-shrink: 0; border-radius: 6px; transition: all 0.15s;
|
||||
}
|
||||
.popup-image-btn:hover { color: var(--primary); background: rgba(7,193,96,0.06); }
|
||||
.popup-image-btn svg { width: 16px; height: 16px; }
|
||||
|
||||
/* 图片预览条 */
|
||||
.popup-image-previews {
|
||||
display: flex; gap: 6px; padding: 4px 12px; overflow-x: auto; flex-shrink: 0;
|
||||
}
|
||||
.popup-img-item { position: relative; flex-shrink: 0; }
|
||||
.popup-img-thumb { width: 40px; height: 40px; border-radius: 6px; object-fit: cover; border: 1px solid #e8e8e8; }
|
||||
.popup-img-remove {
|
||||
position: absolute; top: -4px; right: -4px;
|
||||
width: 14px; height: 14px; border-radius: 50%; background: rgba(0,0,0,0.5); color: #fff;
|
||||
font-size: 10px; line-height: 14px; text-align: center; cursor: pointer; border: none; padding: 0;
|
||||
}
|
||||
.popup-img-remove:hover { background: #e44; }
|
||||
|
||||
/* Settings Overlay */
|
||||
.settings-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.3); display: none; z-index: 100; backdrop-filter: blur(2px); }
|
||||
.settings-overlay.show { display: flex; align-items: center; justify-content: center; }
|
||||
.settings-panel { background: var(--white); border-radius: 14px; padding: 18px 20px; width: 340px; box-shadow: 0 8px 32px rgba(0,0,0,0.15); }
|
||||
.settings-handle { display: none; }
|
||||
.settings-title { font-size: 15px; font-weight: 700; margin-bottom: 12px; color: var(--text); display: flex; align-items: center; justify-content: space-between; }
|
||||
.settings-close { border: none; background: none; color: var(--text3); cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 8px; }
|
||||
.settings-close:hover { background: #f0f0f0; color: var(--text); }
|
||||
.settings-close svg { width: 16px; height: 16px; }
|
||||
.settings-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f2f2f2; }
|
||||
.settings-item:last-child { border-bottom: none; }
|
||||
.settings-label { font-size: 13px; color: var(--text3); }
|
||||
.settings-value { font-size: 13px; color: var(--text); font-weight: 500; max-width: 200px; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.settings-logout { width: 100%; height: 40px; border: none; border-radius: 10px; background: #fff0f0; color: #e44; font-size: 13px; font-weight: 500; cursor: pointer; margin-top: 12px; transition: all 0.2s; }
|
||||
.settings-logout:hover { background: #ffe5e5; }
|
||||
.settings-item-clickable { cursor: pointer; border-radius: 6px; margin: 0 -6px; padding: 10px 6px !important; transition: background 0.15s; }
|
||||
.settings-item-clickable:hover { background: rgba(7,193,96,0.06); }
|
||||
.settings-item-clickable .settings-value { color: var(--primary); }
|
||||
.clip-kb-card { padding: 8px 10px; border: 1px solid #eee; border-radius: 8px; margin-bottom: 6px; cursor: pointer; transition: all 0.15s; }
|
||||
.clip-kb-card:hover { border-color: var(--primary); background: rgba(7,193,96,0.03); }
|
||||
.clip-kb-card.selected { border-color: var(--primary); background: rgba(7,193,96,0.06); }
|
||||
.clip-kb-card-name { font-size: 12px; font-weight: 600; color: var(--text); margin-bottom: 1px; }
|
||||
.clip-kb-card-desc { font-size: 10px; color: var(--text3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.clip-kb-card-meta { font-size: 10px; color: #ccc; margin-top: 2px; }
|
||||
|
||||
/* Toast */
|
||||
.toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%) translateY(-80px); background: var(--text); color: #fff; padding: 10px 22px; border-radius: 22px; font-size: 13px; z-index: 200; transition: transform 0.3s ease; pointer-events: none; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
||||
.toast.show { transform: translateX(-50%) translateY(0); }
|
||||
.toast.success { background: var(--primary); }
|
||||
.toast.error { background: #e44; }
|
||||
|
||||
/* === Markdown 渲染样式 === */
|
||||
.md-pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 10px 12px; overflow-x: auto; font-size: 12px; margin: 6px 0; line-height: 1.5; }
|
||||
.md-pre code { font-family: "SF Mono", "Fira Code", "Consolas", monospace; white-space: pre; }
|
||||
.md-code { background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: "SF Mono", "Fira Code", "Consolas", monospace; }
|
||||
.md-h { margin: 8px 0 4px; line-height: 1.4; }
|
||||
h1.md-h { font-size: 18px; font-weight: 700; }
|
||||
h2.md-h { font-size: 16px; font-weight: 700; }
|
||||
h3.md-h { font-size: 14px; font-weight: 600; }
|
||||
h4.md-h, h5.md-h, h6.md-h { font-size: 13px; font-weight: 600; }
|
||||
.md-bq { border-left: 3px solid var(--primary); padding: 4px 12px; margin: 6px 0; background: #f8faf8; color: #666; border-radius: 0 4px 4px 0; font-size: 13px; }
|
||||
.md-list { margin: 4px 0; padding-left: 20px; font-size: 13px; line-height: 1.6; }
|
||||
.md-list li { margin: 2px 0; }
|
||||
.md-list .md-list { margin: 2px 0 2px 0; }
|
||||
.md-p { margin: 4px 0; line-height: 1.6; font-size: 13px; }
|
||||
.md-p a, .md-list a, .md-bq a { color: var(--primary); text-decoration: none; }
|
||||
.md-p a:hover, .md-list a:hover { text-decoration: underline; }
|
||||
.md-table { border-collapse: collapse; margin: 6px 0; font-size: 12px; width: 100%; }
|
||||
.md-table th { border: 1px solid #e5e5e5; padding: 4px 8px; background: #f7f8fa; font-weight: 600; text-align: left; }
|
||||
.md-table td { border: 1px solid #e5e5e5; padding: 4px 8px; }
|
||||
.md-table tr:hover td { background: rgba(7,193,96,0.02); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Page 1: 登录选择 -->
|
||||
<div id="pg-login" class="page active">
|
||||
<div class="logo">
|
||||
<img src="icons/icon48.png" alt="快速笔记 5.0">
|
||||
</div>
|
||||
<h1>快速笔记 5.0</h1>
|
||||
<p class="subtitle">知识库问答 · 网页采集 · Markdown 速记</p>
|
||||
<button id="btn-ka" class="login-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
快速笔记 5.0 登录
|
||||
</button>
|
||||
<button id="btn-wk" class="login-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||
WeKnora 自建服务
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Page 2: 快速笔记 5.0 登录(扫码 + API Key) -->
|
||||
<div id="pg-ka-login" class="page">
|
||||
<div class="ka-header">
|
||||
<button class="ka-back" id="btn-ka-back" title="返回">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
</button>
|
||||
<span class="ka-title">快速笔记 5.0 登录</span>
|
||||
</div>
|
||||
|
||||
<!-- 登录方式 Tab -->
|
||||
<div class="ka-tabs">
|
||||
<button class="ka-tab active" data-tab="qr">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><circle cx="17.5" cy="17.5" r="3.5"/></svg>
|
||||
微信扫码
|
||||
</button>
|
||||
<button class="ka-tab" data-tab="apikey">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
|
||||
服务地址
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="ka-body">
|
||||
<!-- 扫码登录面板 -->
|
||||
<div class="ka-panel active" id="panel-qr">
|
||||
<div class="qr-wrap">
|
||||
<div class="qr-box" id="qr-box">
|
||||
<div class="qr-placeholder" id="qr-canvas-area"></div>
|
||||
</div>
|
||||
<div class="qr-tip">微信扫码登录</div>
|
||||
<div class="qr-sub">打开微信扫描二维码,或点击二维码模拟登录</div>
|
||||
<div class="qr-status" id="qr-status">
|
||||
<span class="dot"></span>
|
||||
<span id="qr-status-text">等待扫码…</span>
|
||||
</div>
|
||||
<button class="qr-refresh" id="btn-qr-refresh" style="display:none;">刷新二维码</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key 登录面板 -->
|
||||
<div class="ka-panel" id="panel-apikey">
|
||||
<div class="ka-form">
|
||||
<div class="form-group">
|
||||
<label>服务地址</label>
|
||||
<input type="url" id="ka-url" placeholder="https://your-server.com/api">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="ka-key" placeholder="请输入 API Key">
|
||||
</div>
|
||||
<div class="ka-test-msg" id="ka-test-msg"></div>
|
||||
<button class="btn-primary" id="btn-ka-save">保存并登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page 3: WeKnora 配置 -->
|
||||
<div id="pg-weknora" class="page">
|
||||
<div class="wk-header">
|
||||
<h2>WeKnora 自建服务</h2>
|
||||
<p>请填写您的服务配置信息</p>
|
||||
</div>
|
||||
<div class="wk-form">
|
||||
<div class="form-group">
|
||||
<label>服务地址</label>
|
||||
<input type="url" id="wk-url" placeholder="https://your-server.com/api">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="wk-key" placeholder="请输入 API Key">
|
||||
</div>
|
||||
<div class="wk-test-result" id="wk-test-msg"></div>
|
||||
<div class="wk-help">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
<span>不知道如何获取?查看 <a href="#" id="wk-help-link" target="_blank">配置指南</a></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wk-actions">
|
||||
<button class="btn-primary" id="btn-wk-save">保存并登录</button>
|
||||
<button class="btn-secondary" id="btn-wk-back">返回</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page 4: 主界面 -->
|
||||
<div id="pg-main" class="page">
|
||||
<!-- 顶栏 -->
|
||||
<div class="main-top">
|
||||
<div class="main-avatar" id="main-avatar">
|
||||
<div class="letter-avatar" id="main-letter" style="display:none;">S</div>
|
||||
<img id="main-avatar-img" src="" alt="" style="display:none;">
|
||||
</div>
|
||||
<div class="main-info">
|
||||
<div class="main-name" id="main-name">用户</div>
|
||||
<span class="main-badge badge-ka" id="main-badge">快速笔记 5.0</span>
|
||||
</div>
|
||||
<div class="main-actions">
|
||||
<button class="more-btn" id="btn-more" title="更多">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>
|
||||
</button>
|
||||
<div class="more-menu" id="more-menu">
|
||||
<button class="more-menu-item" id="btn-clips">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 016.5 2H19v16H6.5a2.5 2.5 0 000 5H19"/></svg>
|
||||
笔记
|
||||
</button>
|
||||
<button class="more-menu-item" id="btn-sidebar">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
对话面板
|
||||
</button>
|
||||
<button class="more-menu-item" id="btn-settings">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content">
|
||||
|
||||
<!-- ★ 知识采集 — 翻转卡片 + 浮动按钮 -->
|
||||
<div class="collect-wrap" id="collect-wrap">
|
||||
<div class="collect-card-flipper" id="card-flipper">
|
||||
<!-- 正面:最新笔记内容 -->
|
||||
<div class="collect-card-front" id="latest-clip">
|
||||
<!-- 无内容时的占位 -->
|
||||
<div class="collect-empty" id="latest-clip-empty">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="9" y1="8" x2="17" y2="8"/><line x1="9" y1="12" x2="15" y2="12"/></svg>
|
||||
<span>暂无采集内容</span>
|
||||
<span class="collect-empty-sub">使用下方按钮开始采集知识</span>
|
||||
</div>
|
||||
<!-- 有内容时 -->
|
||||
<div class="collect-content" id="latest-clip-content" style="display:none;">
|
||||
<div class="collect-content-top">
|
||||
<div class="collect-content-meta">
|
||||
<span class="latest-clip-tag" id="latest-clip-tag"></span>
|
||||
<span class="latest-clip-time" id="latest-clip-time"></span>
|
||||
</div>
|
||||
<button class="collect-content-more" id="btn-latest-more" title="查看全部">
|
||||
全部
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="collect-content-body" id="latest-clip-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 背面:速记输入框 -->
|
||||
<div class="collect-card-back" id="note-back">
|
||||
<div class="note-back-header">
|
||||
<div class="note-back-title">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
Markdown 速记
|
||||
</div>
|
||||
<button class="note-back-close" id="btn-note-close" title="取消">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="note-card">
|
||||
<textarea class="note-input" id="note-input" placeholder="# 标题 在这里快速记录…"></textarea>
|
||||
<div class="note-preview" id="note-preview"></div>
|
||||
<div class="note-toolbar">
|
||||
<!-- 文字格式组 -->
|
||||
<div class="note-toolbar-group">
|
||||
<button class="note-tool-btn" data-md="bold" title="加粗 **文本**">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="note-toolbar-divider"></div>
|
||||
<!-- 块级元素组 -->
|
||||
<div class="note-toolbar-group">
|
||||
<button class="note-tool-btn" data-md="heading" title="标题 ## 文本">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4H5z"/></svg>
|
||||
</button>
|
||||
<button class="note-tool-btn" data-md="ul" title="无序列表 - 项目">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>
|
||||
</button>
|
||||
<button class="note-tool-btn" data-md="ol" title="有序列表 1. 项目">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="note-toolbar-divider"></div>
|
||||
<!-- 插入组 -->
|
||||
<div class="note-toolbar-group">
|
||||
<button class="note-tool-btn" data-md="link" title="插入链接 [文本](url)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||
</button>
|
||||
<button class="note-tool-btn" data-md="image" title="插入图片 ">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="note-toolbar-divider"></div>
|
||||
<!-- 预览 -->
|
||||
<div class="note-toolbar-group">
|
||||
<button class="note-tool-btn" id="btn-note-preview" data-md="preview" title="预览 Markdown 效果">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="note-toolbar-spacer"></div>
|
||||
<!-- 保存按钮 -->
|
||||
<button class="note-save active" id="btn-note-save" title="保存笔记">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 浮动椭圆胶囊按钮 -->
|
||||
<div class="collect-fab" id="collect-fab">
|
||||
<button class="collect-fab-btn" id="btn-smart-clip" title="智剪">
|
||||
<svg viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
||||
</button>
|
||||
<button class="collect-fab-btn fab-primary" id="btn-select-clip" title="剪藏">
|
||||
<svg viewBox="0 0 24 24"><path d="M3 3h5v2H5v3H3V3zm13 0h5v5h-2V5h-3V3zM3 16v5h5v-2H5v-3H3zm18 0v5h-5v-2h3v-3h2zM7 7h10v10H7V7z"/></svg>
|
||||
</button>
|
||||
<button class="collect-fab-btn" id="btn-quick-note" title="速记">
|
||||
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="popup-image-previews" id="popup-image-previews" style="display:none;"></div>
|
||||
<div class="chat-bar-wrapper">
|
||||
<div class="chat-bar-toolbar">
|
||||
<div class="kb-dropdown" id="kb-dropdown">
|
||||
<button class="chat-bar-kb" id="btn-kb-select">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
|
||||
<span id="kb-name">全部</span>
|
||||
<svg class="chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="kb-dropdown-menu" id="kb-menu">
|
||||
<div class="kb-dropdown-item selected" data-kb="all">
|
||||
<span class="kb-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
||||
全部知识库
|
||||
</div>
|
||||
<div class="kb-dropdown-item" data-kb="quick-note">
|
||||
<span class="kb-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
快速笔记
|
||||
</div>
|
||||
<div class="kb-dropdown-item" data-kb="web-collect">
|
||||
<span class="kb-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
|
||||
网页收藏
|
||||
</div>
|
||||
<div class="kb-dropdown-item" data-kb="product-doc">
|
||||
<span class="kb-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
|
||||
产品文档
|
||||
</div>
|
||||
<div class="kb-dropdown-item" data-kb="tech-spec">
|
||||
<span class="kb-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
||||
技术规范
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="chat-bar-mode" id="btn-mode-select">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||
<span id="mode-name">快速回答</span>
|
||||
<svg class="chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="popup-mode-menu" id="popup-mode-menu">
|
||||
<div class="kb-mode-item selected" data-mode="fast">
|
||||
<span class="kb-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||
快速
|
||||
</div>
|
||||
<div class="kb-mode-item" data-mode="deep">
|
||||
<span class="kb-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
深度
|
||||
</div>
|
||||
<div class="kb-mode-item" data-mode="precise">
|
||||
<span class="kb-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
|
||||
精确
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-bar">
|
||||
<button class="popup-image-btn" id="popup-image-btn" style="display:none;" title="上传图片">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
</button>
|
||||
<textarea class="chat-bar-input" id="chat-input" placeholder="向知识库提问…" autocomplete="off" rows="1"></textarea>
|
||||
<button class="chat-bar-send" id="btn-chat-send">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="popup-image-input" accept="image/jpeg,image/png,image/gif,image/webp" multiple style="display:none;">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设置面板 -->
|
||||
<div class="settings-overlay" id="settings-overlay">
|
||||
<div class="settings-panel">
|
||||
<div class="settings-handle"></div>
|
||||
<div class="settings-title">
|
||||
<span>设置</span>
|
||||
<button class="settings-close" id="btn-settings-close">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="settings-content">
|
||||
<div class="settings-item">
|
||||
<span class="settings-label">账号类型</span>
|
||||
<span class="settings-value" id="set-type">-</span>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<span class="settings-label">用户微信</span>
|
||||
<span class="settings-value" id="set-name">-</span>
|
||||
</div>
|
||||
<div class="settings-item" id="set-url-row" style="display:none;">
|
||||
<span class="settings-label">服务地址</span>
|
||||
<span class="settings-value" id="set-url">-</span>
|
||||
</div>
|
||||
<div class="settings-item" id="set-key-row" style="display:none;">
|
||||
<span class="settings-label">API Key</span>
|
||||
<span class="settings-value" id="set-key">-</span>
|
||||
</div>
|
||||
<div class="settings-item settings-item-clickable" id="set-clip-kb-row">
|
||||
<span class="settings-label">剪藏知识库</span>
|
||||
<span class="settings-value" id="set-clip-kb">未选择 ›</span>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<span class="settings-label">版本</span>
|
||||
<span class="settings-value">v5.0.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="settings-logout" id="btn-logout">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 剪藏知识库选择面板 -->
|
||||
<div class="settings-overlay" id="clip-kb-overlay">
|
||||
<div class="settings-panel" style="max-height:300px;display:flex;flex-direction:column;width:300px;padding:14px 16px;">
|
||||
<div class="settings-title" style="display:flex;align-items:center;justify-content:space-between;font-size:14px;margin-bottom:8px;">
|
||||
<span>选择剪藏知识库</span>
|
||||
<button id="clip-kb-close" style="border:none;background:none;font-size:18px;color:#999;cursor:pointer;padding:0 4px;">×</button>
|
||||
</div>
|
||||
<div class="clip-kb-desc" style="font-size:11px;color:#999;margin-bottom:8px;">剪藏的网页内容将存储到此知识库</div>
|
||||
<div id="clip-kb-list" style="flex:1;overflow-y:auto;"></div>
|
||||
<div id="clip-kb-empty" style="display:none;text-align:center;padding:16px 0;">
|
||||
<div style="font-size:13px;color:#999;margin-bottom:8px;">暂无知识库</div>
|
||||
<div style="font-size:12px;color:#bbb;margin-bottom:12px;">请前往 WeKnora 创建知识库后使用剪藏功能</div>
|
||||
<button id="clip-kb-goto" style="height:32px;padding:0 16px;border:none;border-radius:16px;background:#07C160;color:#fff;font-size:12px;cursor:pointer;">前往创建</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,127 +0,0 @@
|
||||
# 知识助理 - Chrome 浏览器扩展插件需求文档
|
||||
|
||||
## 1. 产品概述
|
||||
|
||||
**产品名称**:知识助理
|
||||
**产品形态**:Chrome 浏览器扩展插件(Manifest V3)
|
||||
**核心定位**:知识库问答 + 网页内容快速采集 + Markdown 速记
|
||||
|
||||
## 2. 设计规范
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 设计风格 | 扁平化 |
|
||||
| 主体色 | `#07C160` |
|
||||
| 图标 | 绿色背景 + 白色线条书本图标 |
|
||||
| 字体 | 系统默认(PingFang SC / Microsoft YaHei) |
|
||||
|
||||
## 3. 功能模块
|
||||
|
||||
### 3.1 双账号登录
|
||||
|
||||
插件安装后首页展示两种账号登录入口:
|
||||
|
||||
#### 3.1.1 知识助理账号
|
||||
|
||||
- 使用微信头像账号体系
|
||||
- 点击后**自动登录**(读取微信授权信息,无需手动操作)
|
||||
- 登录过程展示头像和账号信息的加载过渡
|
||||
|
||||
#### 3.1.2 WeKnora 自建服务账号
|
||||
|
||||
- 需要手动填写**服务地址**和 **API Key**
|
||||
- 提供「测试连接」功能,验证配置是否正确
|
||||
- 填写完成并验证通过后点击「保存并登录」进入主界面
|
||||
|
||||
### 3.2 登录后主界面
|
||||
|
||||
登录成功后进入主界面,顶部显示:
|
||||
|
||||
| 元素 | 说明 |
|
||||
|------|------|
|
||||
| 头像 | 知识助理账号显示微信头像;WeKnora 账号显示字母头像(蓝底白字 S) |
|
||||
| 账号名 | 显示用户昵称或微信号 |
|
||||
| 账号标签 | 标识当前是「知识助理」还是「WeKnora」 |
|
||||
| 设置按钮 | 点击弹出设置面板 |
|
||||
| 退出按钮 | 退出当前账号,返回登录选择页 |
|
||||
|
||||
### 3.3 设置面板
|
||||
|
||||
- 点击设置按钮后,以**底部弹出面板(Bottom Sheet)**形式展示
|
||||
- 内容包含:
|
||||
- 用户头像与账号信息
|
||||
- 知识助理账号:显示微信昵称、微信号
|
||||
- WeKnora 账号:显示服务地址、API Key(可编辑修改)
|
||||
|
||||
### 3.4 知识库对话
|
||||
|
||||
主界面核心功能区,提供**聊天输入框**:
|
||||
|
||||
- 文本输入区域,支持多行输入
|
||||
- 工具栏按钮:知识库选择、快速模式切换
|
||||
- 发送按钮(有内容时激活为绿色)
|
||||
- 发送后打开侧边栏对话面板,展示问答结果
|
||||
- 支持快捷键发送(Enter 发送,Shift+Enter 换行)
|
||||
|
||||
### 3.5 网页内容采集
|
||||
|
||||
#### 3.5.1 保存当前页面
|
||||
|
||||
- 选择目标知识库
|
||||
- 一键保存当前页面 URL 到知识库
|
||||
|
||||
#### 3.5.2 智能剪藏(全窗口截取)
|
||||
|
||||
- 自动提取当前页面的核心内容
|
||||
- AI 智能识别正文、去除广告和导航等干扰信息
|
||||
- 提取结果保存到知识库
|
||||
|
||||
#### 3.5.3 选择剪藏(部分窗口截取)
|
||||
|
||||
- 用户在页面上手动框选/圈选想要保存的区域
|
||||
- 支持选中文字内容提取
|
||||
- 选中内容保存到知识库
|
||||
|
||||
### 3.6 Markdown 快速记录
|
||||
|
||||
- 支持在插件内快速编写 Markdown 内容
|
||||
- 编写完成后保存到指定知识库
|
||||
- 作为快速笔记/速记工具使用
|
||||
|
||||
## 4. 交互流程
|
||||
|
||||
```
|
||||
安装插件
|
||||
│
|
||||
▼
|
||||
首页:选择登录方式
|
||||
├── 知识助理 → 自动获取微信信息 → 自动登录 → 主界面
|
||||
└── WeKnora → 填写服务地址 + API Key → 登录 → 主界面
|
||||
|
||||
主界面
|
||||
├── 对话输入框 → 发送问题 → 侧边栏显示回答
|
||||
├── 保存当前页面 → 选择知识库 → 保存
|
||||
├── 智能剪藏 → 自动提取页面内容 → 保存
|
||||
├── 选择剪藏 → 手动框选区域 → 保存
|
||||
├── Markdown 速记 → 编写内容 → 保存到知识库
|
||||
├── 设置 → 查看/修改账号信息
|
||||
└── 退出 → 返回登录选择页
|
||||
```
|
||||
|
||||
## 5. 快捷键
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
|--------|------|
|
||||
| `⌘/Ctrl + Shift + K` | 打开侧边栏 |
|
||||
| `⌘/Ctrl + Shift + A` | 快速提问 |
|
||||
|
||||
## 6. 技术栈
|
||||
|
||||
| 项目 | 技术选型 |
|
||||
|------|----------|
|
||||
| 扩展标准 | Chrome Extension Manifest V3 |
|
||||
| 前端 | HTML + CSS + JS(外部文件,符合 CSP) |
|
||||
| 状态存储 | chrome.storage.local |
|
||||
| 消息通信 | chrome.runtime.sendMessage |
|
||||
| 侧边栏 | chrome.sidePanel API |
|
||||
| 内容脚本 | content.js + content.css |
|
||||
@@ -1,634 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>快速笔记 5.0 - 对话</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root { --primary: #07C160; --primary-dark: #06ad54; --bg: #f5f6f7; --white: #fff; --text: #333; --text2: #666; --text3: #999; --border: #e5e5e5; --shadow: 0 2px 12px rgba(0,0,0,0.08); --radius: 12px; }
|
||||
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column; }
|
||||
|
||||
/* ===== Header ===== */
|
||||
.header {
|
||||
background: var(--white);
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
.header-icon { width: 30px; height: 30px; background: var(--primary); border-radius: 10px; display: flex; align-items: center; justify-content: center; }
|
||||
.header-icon svg { width: 16px; height: 16px; color: #fff; }
|
||||
.header h1 { font-size: 15px; font-weight: 600; flex: 1; min-width: 0; }
|
||||
.header-title-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; justify-content: center; }
|
||||
.header-session-title { font-size: 11px; font-weight: 400; color: var(--text3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: none; margin-top: 1px; }
|
||||
.header-session-title.show { display: block; }
|
||||
.header-btn { width: 32px; height: 32px; border: none; background: #f5f6f7; border-radius: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); transition: all 0.2s; }
|
||||
.header-btn:hover { background: #eee; color: var(--text); }
|
||||
.header-btn svg { width: 16px; height: 16px; }
|
||||
|
||||
/* ===== Messages ===== */
|
||||
.messages { flex: 1; overflow-y: auto; padding: 18px 16px; display: flex; flex-direction: column; gap: 14px; }
|
||||
|
||||
.msg {
|
||||
max-width: 85%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.msg-user {
|
||||
align-self: flex-end;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 6px;
|
||||
box-shadow: 0 2px 10px rgba(7, 193, 96, 0.2);
|
||||
}
|
||||
.msg-bot {
|
||||
align-self: flex-start;
|
||||
background: var(--white);
|
||||
color: var(--text);
|
||||
border-bottom-left-radius: 6px;
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.08);
|
||||
}
|
||||
.msg-bot pre { background: #f5f5f5; padding: 10px 12px; border-radius: 8px; overflow-x: auto; font-size: 12px; margin: 8px 0; }
|
||||
.msg-bot code { font-family: "SF Mono", "Fira Code", monospace; font-size: 13px; }
|
||||
.msg-time { font-size: 11px; color: var(--text3); text-align: center; padding: 4px 0; }
|
||||
|
||||
.msg-typing {
|
||||
align-self: flex-start;
|
||||
background: var(--white);
|
||||
padding: 14px 20px;
|
||||
border-radius: 16px;
|
||||
border-bottom-left-radius: 6px;
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.08);
|
||||
display: none;
|
||||
}
|
||||
.msg-typing.show { display: flex; }
|
||||
.typing-dots { display: flex; gap: 5px; }
|
||||
.typing-dots span { width: 6px; height: 6px; background: var(--text3); border-radius: 50%; animation: typing-bounce 1.4s infinite; }
|
||||
.typing-dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.typing-dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||
@keyframes typing-bounce { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-6px); } }
|
||||
|
||||
/* ===== Welcome ===== */
|
||||
.welcome {
|
||||
text-align: center;
|
||||
padding: 36px 20px;
|
||||
background: var(--white);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 0 8px rgba(0,0,0,0.04), 0 4px 20px rgba(0,0,0,0.08);
|
||||
margin: 8px 0;
|
||||
}
|
||||
.welcome-icon { width: 56px; height: 56px; background: #e8f8ee; border-radius: 16px; display: flex; align-items: center; justify-content: center; margin: 0 auto 14px; }
|
||||
.welcome-icon svg { width: 28px; height: 28px; color: var(--primary); }
|
||||
.welcome h2 { font-size: 16px; font-weight: 600; margin-bottom: 6px; }
|
||||
.welcome p { font-size: 13px; color: var(--text3); line-height: 1.5; }
|
||||
.quick-btns { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 16px; }
|
||||
.quick-btn {
|
||||
padding: 7px 16px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
background: #f5f6f7;
|
||||
font-size: 12px;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||||
}
|
||||
.quick-btn:hover { background: rgba(7,193,96,0.08); color: var(--primary); box-shadow: 0 2px 8px rgba(7,193,96,0.12); }
|
||||
|
||||
/* ===== Input Area ===== */
|
||||
.input-area {
|
||||
padding: 10px 14px 14px;
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 整体包裹卡片 */
|
||||
.sp-chat-bar-wrapper {
|
||||
background: var(--white);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 20px rgba(0,0,0,0.10);
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.sp-chat-bar-wrapper:focus-within {
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.10), 0 0 0 1.5px rgba(7,193,96,0.18);
|
||||
}
|
||||
|
||||
/* 顶部工具栏:知识库 + 模式 */
|
||||
.sp-chat-bar-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 8px 0;
|
||||
}
|
||||
|
||||
.sp-chat-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 5px 5px 8px;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
}
|
||||
/* 知识库选择按钮 */
|
||||
.sp-chat-bar-kb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
border: none;
|
||||
background: #f3f4f5;
|
||||
border-radius: 50px;
|
||||
padding: 6px 8px 6px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sp-chat-bar-kb:hover { background: #eaebed; color: var(--text); }
|
||||
.sp-chat-bar-kb svg { width: 13px; height: 13px; flex-shrink: 0; }
|
||||
.sp-chat-bar-arrow { width: 10px !important; height: 10px !important; opacity: 0.5; }
|
||||
|
||||
/* 模式选择按钮 */
|
||||
.sp-chat-bar-mode {
|
||||
display: flex; align-items: center; gap: 3px;
|
||||
border: none; background: #f3f4f5; border-radius: 50px;
|
||||
padding: 6px 8px 6px 10px; font-size: 11px; font-weight: 500;
|
||||
color: var(--text2); cursor: pointer; white-space: nowrap;
|
||||
transition: all 0.15s; flex-shrink: 0;
|
||||
}
|
||||
.sp-chat-bar-mode:hover { background: #eaebed; color: var(--text); }
|
||||
.sp-chat-bar-mode svg { width: 13px; height: 13px; flex-shrink: 0; }
|
||||
|
||||
/* 模式下拉菜单 */
|
||||
.sp-mode-dropdown-menu {
|
||||
position: fixed; background: var(--white); border-radius: 14px;
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 8px 32px rgba(0,0,0,0.14);
|
||||
padding: 6px 0; min-width: 160px; display: none; z-index: 100;
|
||||
}
|
||||
.sp-mode-dropdown-menu.show { display: block; }
|
||||
|
||||
/* 分隔竖线 */
|
||||
.sp-chat-bar-divider { width: 1px; height: 16px; background: #e5e5e5; margin: 0 8px; flex-shrink: 0; }
|
||||
|
||||
/* 输入框 */
|
||||
.sp-chat-bar-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
color: var(--text);
|
||||
padding: 6px 4px;
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
max-height: 120px;
|
||||
resize: none;
|
||||
overflow-y: auto;
|
||||
line-height: 18px;
|
||||
}
|
||||
.sp-chat-bar-input::placeholder { color: #c0c0c0; }
|
||||
|
||||
/* 发送按钮 — 圆形 */
|
||||
.sp-chat-bar-send {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: #e8e8e8;
|
||||
color: #aaa;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sp-chat-bar-send svg { width: 14px; height: 14px; }
|
||||
.sp-chat-bar-send.active { background: var(--primary); color: #fff; box-shadow: 0 2px 8px rgba(7,193,96,0.25); }
|
||||
.sp-chat-bar-send.active:hover { background: var(--primary-dark); }
|
||||
|
||||
/* 图片上传按钮 */
|
||||
.sp-chat-bar-img {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: none; background: none; color: #999; cursor: pointer;
|
||||
width: 28px; height: 28px; padding: 0; flex-shrink: 0; border-radius: 6px; transition: all 0.15s;
|
||||
}
|
||||
.sp-chat-bar-img:hover { color: var(--primary); background: rgba(7,193,96,0.06); }
|
||||
.sp-chat-bar-img svg { width: 16px; height: 16px; }
|
||||
|
||||
/* 图片预览条 */
|
||||
.sp-image-previews {
|
||||
display: flex; gap: 6px; padding: 6px 12px 0; overflow-x: auto; flex-shrink: 0;
|
||||
}
|
||||
.sp-img-item { position: relative; flex-shrink: 0; }
|
||||
.sp-img-thumb { width: 44px; height: 44px; border-radius: 6px; object-fit: cover; border: 1px solid #e8e8e8; }
|
||||
.sp-img-remove {
|
||||
position: absolute; top: -4px; right: -4px;
|
||||
width: 16px; height: 16px; border-radius: 50%; background: rgba(0,0,0,0.5); color: #fff;
|
||||
font-size: 12px; line-height: 16px; text-align: center; cursor: pointer; border: none; padding: 0;
|
||||
}
|
||||
.sp-img-remove:hover { background: #e44; }
|
||||
|
||||
/* 用户消息中的图片 */
|
||||
.msg-user-images { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
|
||||
.msg-user-img { width: 60px; height: 60px; border-radius: 8px; object-fit: cover; border: 1px solid rgba(255,255,255,0.3); cursor: pointer; }
|
||||
.msg-user-img:hover { opacity: 0.85; }
|
||||
|
||||
/* ===== 合并下拉菜单 (fixed定位) ===== */
|
||||
.sp-kb-dropdown-menu {
|
||||
position: fixed;
|
||||
background: var(--white);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 8px 32px rgba(0,0,0,0.14);
|
||||
padding: 6px 0;
|
||||
min-width: 200px;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.sp-kb-dropdown-menu.show { display: block; }
|
||||
|
||||
.sp-dropdown-item {
|
||||
padding: 9px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.sp-dropdown-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
|
||||
.sp-dropdown-item.selected { color: var(--primary); font-weight: 500; }
|
||||
.sp-dropdown-item .sp-radio {
|
||||
width: 16px; height: 16px;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.sp-dropdown-item.selected .sp-radio { border-color: var(--primary); }
|
||||
.sp-dropdown-item.selected .sp-radio::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--primary); }
|
||||
.sp-dropdown-item:hover .sp-radio { border-color: var(--primary); }
|
||||
.sp-dropdown-item svg { width: 14px; height: 14px; }
|
||||
|
||||
/* 下拉菜单分隔线和分组标题 */
|
||||
.sp-dropdown-divider { height: 1px; background: #f0f0f0; margin: 4px 12px; }
|
||||
.sp-dropdown-section { padding: 6px 14px 2px; font-size: 10px; font-weight: 600; color: #bbb; letter-spacing: 0.5px; text-transform: uppercase; }
|
||||
|
||||
/* 模式选项 */
|
||||
.sp-mode-item {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.sp-mode-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
|
||||
.sp-mode-item.selected { color: var(--primary); font-weight: 500; }
|
||||
.sp-mode-item .sp-radio {
|
||||
width: 14px; height: 14px;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.sp-mode-item.selected .sp-radio { border-color: var(--primary); }
|
||||
.sp-mode-item.selected .sp-radio::after { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--primary); }
|
||||
.sp-mode-item:hover .sp-radio { border-color: var(--primary); }
|
||||
.sp-mode-item svg { width: 14px; height: 14px; }
|
||||
|
||||
/* 附件选项 */
|
||||
.sp-attach-item {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.sp-attach-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
|
||||
.sp-attach-item svg { width: 14px; height: 14px; }
|
||||
|
||||
/* ===== Clips ===== */
|
||||
.clip-card {
|
||||
background: var(--white);
|
||||
border-radius: 16px;
|
||||
padding: 14px 16px;
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.08);
|
||||
align-self: flex-start;
|
||||
max-width: 90%;
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.clip-card:hover { box-shadow: 0 0 8px rgba(0,0,0,0.06), 0 6px 24px rgba(0,0,0,0.12); }
|
||||
.clip-card-title { font-size: 12px; color: var(--primary); font-weight: 600; margin-bottom: 8px; display: flex; align-items: center; gap: 5px; }
|
||||
.clip-card-title svg { flex-shrink: 0; }
|
||||
.clip-card-content { font-size: 13px; color: var(--text); line-height: 1.65; max-height: 200px; overflow-y: auto; }
|
||||
.clip-actions { display: flex; gap: 8px; margin-top: 10px; }
|
||||
.clip-action-btn {
|
||||
padding: 5px 14px;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
background: #f5f6f7;
|
||||
font-size: 11px;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.clip-action-btn:hover { background: rgba(7,193,96,0.08); color: var(--primary); }
|
||||
|
||||
/* === Markdown 渲染样式 === */
|
||||
.md-pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 10px 12px; overflow-x: auto; font-size: 12px; margin: 6px 0; line-height: 1.5; }
|
||||
.md-pre code { font-family: "SF Mono", "Fira Code", "Consolas", monospace; white-space: pre; }
|
||||
.md-code { background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: "SF Mono", "Fira Code", "Consolas", monospace; }
|
||||
.md-h { margin: 8px 0 4px; line-height: 1.4; }
|
||||
h1.md-h { font-size: 18px; font-weight: 700; }
|
||||
h2.md-h { font-size: 16px; font-weight: 700; }
|
||||
h3.md-h { font-size: 14px; font-weight: 600; }
|
||||
h4.md-h, h5.md-h, h6.md-h { font-size: 13px; font-weight: 600; }
|
||||
.md-bq { border-left: 3px solid var(--primary); padding: 4px 12px; margin: 6px 0; background: #f8faf8; color: #666; border-radius: 0 4px 4px 0; font-size: 13px; }
|
||||
.md-list { margin: 4px 0; padding-left: 20px; font-size: 13px; line-height: 1.6; }
|
||||
.md-list li { margin: 2px 0; }
|
||||
.md-list .md-list { margin: 2px 0 2px 0; }
|
||||
.md-p { margin: 4px 0; line-height: 1.6; font-size: 13px; }
|
||||
.md-p a, .md-list a, .md-bq a { color: var(--primary); text-decoration: none; }
|
||||
.md-p a:hover, .md-list a:hover { text-decoration: underline; }
|
||||
.md-table { border-collapse: collapse; margin: 6px 0; font-size: 12px; }
|
||||
.md-table th { border: 1px solid #e5e5e5; padding: 4px 8px; background: #f7f8fa; font-weight: 600; text-align: left; white-space: nowrap; }
|
||||
.md-table td { border: 1px solid #e5e5e5; padding: 4px 8px; }
|
||||
.md-table tr:hover td { background: rgba(7,193,96,0.02); }
|
||||
.md-table-wrap { overflow-x: auto; margin: 6px 0; }
|
||||
.md-img { max-width: 100%; max-height: 200px; border-radius: 6px; margin: 6px 0; display: block; object-fit: contain; cursor: pointer; }
|
||||
.md-img:hover { opacity: 0.85; }
|
||||
|
||||
/* 图片灯箱 */
|
||||
.sp-lightbox {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 9999;
|
||||
display: flex; align-items: center; justify-content: center; cursor: zoom-out;
|
||||
}
|
||||
.sp-lightbox img {
|
||||
max-width: 95%; max-height: 95%; object-fit: contain; border-radius: 8px;
|
||||
box-shadow: 0 4px 32px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
/* === Agent 流式显示(参考 AgentStreamDisplay.vue)=== */
|
||||
.agent-stream { display: flex; flex-direction: column; gap: 0; width: 92%; align-self: flex-start; margin-bottom: 6px; }
|
||||
|
||||
/* 折叠的中间步骤 — 树根 */
|
||||
.agent-tree-root {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 6px 14px; font-size: 12px; font-weight: 500; cursor: pointer;
|
||||
border-radius: 8px; background: var(--white); border: 0.5px solid #e0e0e0;
|
||||
box-shadow: 0 2px 4px rgba(7,192,95,0.08); color: var(--text);
|
||||
transition: all 0.25s cubic-bezier(0.4,0,0.2,1); margin-bottom: 10px; user-select: none;
|
||||
}
|
||||
.agent-tree-root:hover { background: rgba(7,192,95,0.04); }
|
||||
.agent-tree-root-title { display: flex; align-items: center; gap: 6px; }
|
||||
.agent-tree-root-title svg { width: 16px; height: 16px; color: var(--primary); flex-shrink: 0; }
|
||||
.agent-tree-root-toggle { font-size: 12px; color: var(--primary); transition: transform 0.2s; }
|
||||
.agent-tree-root.expanded .agent-tree-root-toggle { transform: rotate(180deg); }
|
||||
|
||||
/* 树子节点 */
|
||||
.agent-tree-children { display: none; padding-left: 12px; margin-top: -4px; margin-bottom: 10px; position: relative; }
|
||||
.agent-tree-root.expanded + .agent-tree-children { display: block; }
|
||||
.agent-tree-child { position: relative; padding-left: 20px; margin-bottom: 6px; }
|
||||
.agent-tree-child::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; border-left: 1px dashed #e0e0e0; }
|
||||
.agent-tree-child:last-child::before { bottom: auto; height: 15px; }
|
||||
.agent-tree-branch { position: absolute; left: 0; top: 15px; width: 16px; border-top: 1px dashed #e0e0e0; }
|
||||
|
||||
/* 通用动作卡片(思考/工具调用) */
|
||||
.agent-card {
|
||||
background: var(--white); border-radius: 5px; border: 1px solid #e8e8e8;
|
||||
overflow: hidden; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.02);
|
||||
animation: agentFadeIn 0.25s ease-out; margin-bottom: 8px; position: relative; width: 100%;
|
||||
}
|
||||
.agent-card:hover { border-color: var(--primary); box-shadow: 0 1px 4px rgba(7,192,95,0.08); }
|
||||
.agent-card.pending { border-color: rgba(7,192,95,0.15); overflow: hidden; }
|
||||
.agent-card.pending::after {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: linear-gradient(120deg, transparent 0%, rgba(7,192,95,0.06) 40%, rgba(7,192,95,0.08) 55%, transparent 85%);
|
||||
transform: translateX(-100%); animation: agentShimmer 2.8s ease-in-out infinite; pointer-events: none;
|
||||
}
|
||||
.agent-card.error { border-left: 2px solid #e44; }
|
||||
.agent-card-header {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 5px 10px; cursor: pointer; user-select: none; transition: background-color 0.15s;
|
||||
}
|
||||
.agent-card-header:hover { background: rgba(7,192,95,0.03); }
|
||||
.agent-card-header.no-expand { cursor: default; }
|
||||
.agent-card-header.no-expand:hover { background: transparent; }
|
||||
.agent-card-title { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
|
||||
.agent-card-title svg { width: 14px; height: 14px; color: var(--primary); flex-shrink: 0; }
|
||||
.agent-card-name { font-size: 12px; font-weight: 500; white-space: nowrap; color: var(--text); overflow: hidden; text-overflow: ellipsis; }
|
||||
.agent-card-summary { color: var(--text3); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; margin-left: 4px; }
|
||||
.agent-card-toggle { font-size: 12px; color: var(--text3); transition: transform 0.2s; padding: 0 2px; flex-shrink: 0; }
|
||||
.agent-card.expanded .agent-card-toggle { transform: rotate(180deg); }
|
||||
.agent-card-detail {
|
||||
display: none; padding: 6px 12px; border-top: 1px solid #f0f0f0;
|
||||
font-size: 12px; color: var(--text2); line-height: 1.6; max-height: 200px;
|
||||
overflow-y: auto; background: #fafbfc; white-space: pre-wrap; word-break: break-word;
|
||||
}
|
||||
.agent-card.expanded .agent-card-detail { display: block; }
|
||||
.agent-card-info {
|
||||
padding: 5px 10px; border-top: 1px solid #f0f0f0;
|
||||
font-size: 11px; font-weight: 500; color: var(--text); line-height: 1.5;
|
||||
}
|
||||
.agent-card-info strong { color: var(--primary); font-weight: 600; }
|
||||
|
||||
/* 计划任务变更 */
|
||||
.agent-plan-change {
|
||||
padding: 8px 12px; background: linear-gradient(135deg, rgba(7,192,95,0.05), rgba(7,192,95,0.02));
|
||||
border-radius: 6px; border: 1px solid rgba(7,192,95,0.2);
|
||||
font-size: 12px; color: var(--text); margin-bottom: 8px; animation: agentFadeIn 0.25s ease-out;
|
||||
}
|
||||
.agent-plan-change strong { color: var(--primary); font-weight: 600; margin-right: 3px; }
|
||||
|
||||
/* 回答区域 */
|
||||
.agent-answer { animation: agentFadeIn 0.25s ease-out; }
|
||||
.agent-answer-content {
|
||||
font-size: 14px; color: var(--text); line-height: 1.6; background: var(--white);
|
||||
border-radius: 16px; border-bottom-left-radius: 6px; padding: 12px 16px;
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.08);
|
||||
}
|
||||
.agent-answer-toolbar {
|
||||
display: flex; gap: 6px; margin-top: 6px; padding-left: 2px;
|
||||
}
|
||||
.agent-answer-toolbar button {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 28px; height: 28px; border: 1px solid #e8e8e8; border-radius: 50%;
|
||||
background: var(--white); cursor: pointer; color: var(--text3); transition: all 0.2s;
|
||||
}
|
||||
.agent-answer-toolbar button:hover { border-color: var(--primary); color: var(--primary); background: rgba(7,192,95,0.04); }
|
||||
.agent-answer-toolbar button svg { width: 14px; height: 14px; }
|
||||
|
||||
/* 知识引用面板 */
|
||||
.agent-refs {
|
||||
background: var(--white); border: 1px solid #e8e8e8; border-radius: 10px;
|
||||
margin-top: 8px; overflow: hidden; font-size: 12px;
|
||||
}
|
||||
.agent-refs-header {
|
||||
display: flex; align-items: center; gap: 6px; padding: 8px 12px;
|
||||
cursor: pointer; color: var(--text2); font-weight: 500; user-select: none;
|
||||
}
|
||||
.agent-refs-header:hover { background: rgba(7,193,96,0.04); }
|
||||
.agent-refs-header svg:first-child { color: var(--primary); flex-shrink: 0; }
|
||||
.agent-refs-arrow { margin-left: auto; transition: transform 0.2s; color: var(--text3); }
|
||||
.agent-refs.expanded .agent-refs-arrow { transform: rotate(180deg); }
|
||||
.agent-refs-list {
|
||||
display: none; border-top: 1px solid #f0f0f0; padding: 6px 12px 8px;
|
||||
max-height: 200px; overflow-y: auto;
|
||||
}
|
||||
.agent-refs.expanded .agent-refs-list { display: block; }
|
||||
.agent-ref-group { margin-bottom: 6px; }
|
||||
.agent-ref-group-title { display: flex; align-items: center; gap: 4px; font-weight: 500; color: var(--text); padding: 2px 0; font-size: 12px; }
|
||||
.agent-ref-group-title svg { color: var(--primary); flex-shrink: 0; }
|
||||
.agent-ref-count { color: var(--text3); font-weight: 400; font-size: 11px; }
|
||||
.agent-ref-chunk { color: var(--text3); padding: 2px 0 2px 18px; line-height: 1.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; font-size: 11px; }
|
||||
.agent-ref-chunk:hover { color: var(--primary); background: rgba(7,193,96,0.04); border-radius: 4px; }
|
||||
.agent-ref-idx { color: var(--primary); font-weight: 500; font-size: 10px; }
|
||||
|
||||
/* 动画 */
|
||||
@keyframes agentFadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes agentShimmer { 0% { transform: translateX(-90%); } 50% { transform: translateX(-5%); } 100% { transform: translateX(90%); } }
|
||||
@keyframes agentSpin { to { transform: rotate(360deg); } }
|
||||
.agent-spinner { width: 12px; height: 12px; border: 2px solid #e0e0e0; border-top-color: var(--primary); border-radius: 50%; animation: agentSpin 0.8s linear infinite; display: inline-block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<div class="header-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
</div>
|
||||
<div class="header-title-wrap">
|
||||
<h1>快速笔记 5.0</h1>
|
||||
<div class="header-session-title" id="sp-session-title"></div>
|
||||
</div>
|
||||
<button class="header-btn" id="btn-clear" title="清空对话">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="messages" id="messages">
|
||||
<div class="welcome" id="welcome">
|
||||
<div class="welcome-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
</div>
|
||||
<h2>你好,有什么可以帮助你?</h2>
|
||||
<p>你可以向我提问知识库中的任何问题</p>
|
||||
<div class="quick-btns">
|
||||
<button class="quick-btn" data-q="帮我总结最近的笔记">总结笔记</button>
|
||||
<button class="quick-btn" data-q="知识库中有哪些内容?">知识库概览</button>
|
||||
<button class="quick-btn" data-q="搜索相关文档">搜索文档</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg-typing" id="typing">
|
||||
<div class="typing-dots">
|
||||
<span></span><span></span><span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
<div class="sp-image-previews" id="sp-image-previews" style="display:none;"></div>
|
||||
<div class="sp-chat-bar-wrapper">
|
||||
<div class="sp-chat-bar-toolbar">
|
||||
<button class="sp-chat-bar-kb" id="sp-btn-kb">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
|
||||
<span id="sp-kb-name">全部</span>
|
||||
<svg class="sp-chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<button class="sp-chat-bar-mode" id="sp-btn-mode">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||
<span id="sp-mode-name">快速回答</span>
|
||||
<svg class="sp-chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sp-chat-bar">
|
||||
<button class="sp-chat-bar-img" id="sp-image-btn" style="display:none;" title="上传图片">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
</button>
|
||||
<textarea class="sp-chat-bar-input" id="sp-input" placeholder="向知识库提问…" autocomplete="off" rows="1"></textarea>
|
||||
<button class="sp-chat-bar-send" id="sp-send">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="sp-image-input" accept="image/jpeg,image/png,image/gif,image/webp" multiple style="display:none;">
|
||||
<!-- 合并下拉菜单 -->
|
||||
<div class="sp-kb-dropdown-menu" id="sp-kb-menu">
|
||||
<div class="sp-dropdown-item selected" data-kb="all">
|
||||
<span class="sp-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
||||
全部知识库
|
||||
</div>
|
||||
<div class="sp-dropdown-item" data-kb="quick-note">
|
||||
<span class="sp-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
快速笔记
|
||||
</div>
|
||||
<div class="sp-dropdown-item" data-kb="web-collect">
|
||||
<span class="sp-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
|
||||
网页收藏
|
||||
</div>
|
||||
<div class="sp-dropdown-item" data-kb="product-doc">
|
||||
<span class="sp-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
|
||||
产品文档
|
||||
</div>
|
||||
<div class="sp-dropdown-item" data-kb="tech-spec">
|
||||
<span class="sp-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
||||
技术规范
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模式下拉菜单(独立) -->
|
||||
<div class="sp-mode-dropdown-menu" id="sp-mode-menu">
|
||||
<div class="sp-mode-item selected" data-mode="fast">
|
||||
<span class="sp-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||
快速
|
||||
</div>
|
||||
<div class="sp-mode-item" data-mode="deep">
|
||||
<span class="sp-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
深度
|
||||
</div>
|
||||
<div class="sp-mode-item" data-mode="precise">
|
||||
<span class="sp-radio"></span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
|
||||
精确
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="sidepanel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,387 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WeKnora - 知识库管理</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
:root { --primary: #07C160; --primary-dark: #06ad54; --bg: #f5f6f7; --white: #fff; --text: #1a1a1a; --text2: #555; --text3: #999; --border: #eaeaea; }
|
||||
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--white); color: var(--text); height: 100vh; display: flex; overflow: hidden; }
|
||||
|
||||
/* === 左侧导航 === */
|
||||
.sidebar { width: 220px; background: #fafbfc; border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
|
||||
.sidebar-logo { padding: 20px 20px 24px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.sidebar-logo h1 { font-size: 18px; font-weight: 700; color: var(--text); letter-spacing: 1px; }
|
||||
.sidebar-logo-btn { width: 30px; height: 30px; border: none; background: transparent; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); }
|
||||
.sidebar-logo-btn:hover { background: #eee; color: var(--text); }
|
||||
.sidebar-logo-btn svg { width: 16px; height: 16px; }
|
||||
|
||||
.sidebar-nav { flex: 1; padding: 0 12px; }
|
||||
.nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; font-size: 14px; color: var(--text2); cursor: pointer; transition: all 0.15s; margin-bottom: 2px; position: relative; }
|
||||
.nav-item:hover { background: #f0f0f0; color: var(--text); }
|
||||
.nav-item.active { background: var(--primary); color: #fff; }
|
||||
.nav-item svg { width: 18px; height: 18px; flex-shrink: 0; }
|
||||
.nav-item .nav-plus { position: absolute; right: 10px; width: 18px; height: 18px; color: var(--primary); }
|
||||
.nav-item.active .nav-plus { color: rgba(255,255,255,0.7); }
|
||||
|
||||
.sidebar-user { padding: 16px; border-top: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
|
||||
.sidebar-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--primary); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 15px; font-weight: 600; flex-shrink: 0; }
|
||||
.sidebar-user-info { flex: 1; min-width: 0; }
|
||||
.sidebar-user-name { font-size: 13px; font-weight: 600; color: var(--text); }
|
||||
.sidebar-user-email { font-size: 11px; color: var(--text3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.sidebar-user-arrow { width: 16px; height: 16px; color: var(--text3); }
|
||||
|
||||
/* === 中间内容区 === */
|
||||
.main-area { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 0; }
|
||||
|
||||
/* 内容顶部 tabs */
|
||||
.content-top { padding: 20px 24px 0; }
|
||||
.content-tabs { display: flex; gap: 16px; margin-bottom: 16px; }
|
||||
.content-tab { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 8px 12px; border-radius: 10px; font-size: 11px; color: var(--text3); cursor: pointer; transition: all 0.15s; min-width: 48px; }
|
||||
.content-tab:hover { background: #f5f5f5; color: var(--text2); }
|
||||
.content-tab.active { background: rgba(7,193,96,0.08); color: var(--primary); }
|
||||
.content-tab svg { width: 18px; height: 18px; }
|
||||
|
||||
.content-header { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
|
||||
.content-header h2 { font-size: 22px; font-weight: 700; }
|
||||
.content-header-btn { width: 28px; height: 28px; border: none; background: rgba(7,193,96,0.08); border-radius: 7px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--primary); }
|
||||
.content-header-btn svg { width: 15px; height: 15px; }
|
||||
.content-desc { font-size: 13px; color: var(--text3); margin-bottom: 20px; }
|
||||
|
||||
/* 空状态 */
|
||||
.content-body { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px; position: relative; }
|
||||
.empty-illustration { width: 140px; height: 100px; margin-bottom: 20px; position: relative; }
|
||||
.empty-box { width: 120px; height: 50px; background: linear-gradient(180deg, #e8f8ee 0%, #d4f2e0 100%); border-radius: 8px; position: absolute; bottom: 0; left: 10px; display: flex; align-items: center; justify-content: center; gap: 6px; }
|
||||
.empty-box-dot { width: 8px; height: 8px; border-radius: 50%; }
|
||||
.empty-box-dot:nth-child(1) { background: #2b6bff; }
|
||||
.empty-box-dot:nth-child(2) { background: #2b6bff; }
|
||||
.empty-box-dot:nth-child(3) { background: #e0e0e0; }
|
||||
.empty-arrows { position: absolute; top: 0; left: 50%; transform: translateX(-50%); display: flex; gap: 4px; }
|
||||
.empty-arrow { width: 20px; height: 30px; }
|
||||
.empty-title { font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 6px; }
|
||||
.empty-desc { font-size: 13px; color: var(--text3); margin-bottom: 20px; text-align: center; }
|
||||
.empty-btn { height: 40px; padding: 0 24px; border: none; border-radius: 20px; background: var(--primary); color: #fff; font-size: 14px; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 8px; transition: all 0.2s; }
|
||||
.empty-btn:hover { background: var(--primary-dark); box-shadow: 0 4px 12px rgba(7,193,96,0.25); }
|
||||
.empty-btn svg { width: 16px; height: 16px; }
|
||||
|
||||
/* 右下角 FAB */
|
||||
.fab-btn { position: absolute; right: 24px; bottom: 24px; width: 44px; height: 44px; border: none; border-radius: 50%; background: var(--primary); color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(7,193,96,0.3); transition: all 0.2s; }
|
||||
.fab-btn:hover { transform: scale(1.08); box-shadow: 0 6px 20px rgba(7,193,96,0.35); }
|
||||
.fab-btn svg { width: 22px; height: 22px; }
|
||||
|
||||
/* === 右侧对话面板 === */
|
||||
.chat-panel { width: 360px; display: flex; flex-direction: column; flex-shrink: 0; background: var(--white); }
|
||||
|
||||
/* 对话面板头部 */
|
||||
.chat-panel-header { padding: 14px 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; }
|
||||
.chat-panel-icon { font-size: 16px; }
|
||||
.chat-panel-title { font-size: 14px; font-weight: 600; flex: 1; }
|
||||
.chat-panel-actions { display: flex; gap: 2px; }
|
||||
.chat-panel-btn { width: 30px; height: 30px; border: none; background: transparent; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); }
|
||||
.chat-panel-btn:hover { background: #f5f5f5; color: var(--text); }
|
||||
.chat-panel-btn svg { width: 16px; height: 16px; }
|
||||
|
||||
/* 对话内容 */
|
||||
.chat-panel-brand { padding: 16px; }
|
||||
.chat-panel-brand-name { font-size: 15px; font-weight: 700; color: var(--primary); margin-bottom: 20px; }
|
||||
.chat-panel-brand-bar { display: flex; gap: 6px; }
|
||||
.brand-bar-btn { width: 30px; height: 30px; border: 1px solid var(--border); border-radius: 8px; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); }
|
||||
.brand-bar-btn:hover { border-color: var(--primary); color: var(--primary); }
|
||||
.brand-bar-btn svg { width: 14px; height: 14px; }
|
||||
|
||||
.chat-panel-body { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 30px; text-align: center; }
|
||||
.chat-panel-body h3 { font-size: 16px; font-weight: 600; margin-bottom: 8px; color: var(--text); }
|
||||
.chat-panel-body p { font-size: 13px; color: var(--text3); line-height: 1.5; }
|
||||
|
||||
/* 对话输入框 */
|
||||
.chat-panel-input { padding: 12px 14px; border-top: 1px solid var(--border); }
|
||||
.chat-input-card { background: #f7f8fa; border-radius: 12px; border: 1.5px solid rgba(0,0,0,0.06); transition: border-color 0.2s; }
|
||||
.chat-input-card:focus-within { border-color: rgba(7,193,96,0.3); }
|
||||
.chat-input-card textarea { width: 100%; min-height: 36px; max-height: 80px; border: none; background: transparent; padding: 10px 14px 4px; font-size: 13px; font-family: inherit; resize: none; outline: none; line-height: 1.5; color: var(--text); }
|
||||
.chat-input-card textarea::placeholder { color: #bbb; }
|
||||
.chat-input-toolbar { display: flex; align-items: center; padding: 4px 8px 8px; gap: 2px; }
|
||||
.chat-tool-btn { height: 26px; padding: 0 8px; border: none; background: transparent; border-radius: 6px; font-size: 11px; color: var(--text3); cursor: pointer; display: flex; align-items: center; gap: 4px; transition: all 0.15s; white-space: nowrap; }
|
||||
.chat-tool-btn:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
|
||||
.chat-tool-btn svg { width: 12px; height: 12px; }
|
||||
.chat-tool-btn.mode-active { color: var(--primary); font-weight: 500; }
|
||||
.chat-tool-divider { width: 1px; height: 12px; background: #eee; margin: 0 2px; }
|
||||
|
||||
/* 图片上传按钮 */
|
||||
.wk-image-upload-btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: none; background: none; color: #999; cursor: pointer;
|
||||
width: 26px; height: 26px; padding: 0; border-radius: 6px; transition: all 0.15s;
|
||||
}
|
||||
.wk-image-upload-btn:hover { color: var(--primary); background: rgba(7,193,96,0.06); }
|
||||
.wk-image-upload-btn svg { width: 14px; height: 14px; }
|
||||
|
||||
/* 图片预览条 */
|
||||
.wk-image-previews {
|
||||
display: flex; gap: 6px; padding: 6px 12px; overflow-x: auto; flex-shrink: 0;
|
||||
}
|
||||
.wk-img-item { position: relative; flex-shrink: 0; }
|
||||
.wk-img-thumb { width: 44px; height: 44px; border-radius: 6px; object-fit: cover; border: 1px solid #e8e8e8; }
|
||||
.wk-img-remove {
|
||||
position: absolute; top: -4px; right: -4px;
|
||||
width: 16px; height: 16px; border-radius: 50%; background: rgba(0,0,0,0.5); color: #fff;
|
||||
font-size: 12px; line-height: 16px; text-align: center; cursor: pointer; border: none; padding: 0;
|
||||
}
|
||||
.wk-img-remove:hover { background: #e44; }
|
||||
|
||||
/* === Markdown 渲染样式 === */
|
||||
.md-pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 10px 12px; overflow-x: auto; font-size: 12px; margin: 6px 0; line-height: 1.5; }
|
||||
.md-pre code { font-family: "SF Mono", "Fira Code", "Consolas", monospace; white-space: pre; }
|
||||
.md-code { background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; font-family: "SF Mono", "Fira Code", "Consolas", monospace; }
|
||||
.md-h { margin: 8px 0 4px; line-height: 1.4; }
|
||||
h1.md-h { font-size: 18px; font-weight: 700; }
|
||||
h2.md-h { font-size: 16px; font-weight: 700; }
|
||||
h3.md-h { font-size: 14px; font-weight: 600; }
|
||||
h4.md-h, h5.md-h, h6.md-h { font-size: 13px; font-weight: 600; }
|
||||
.md-bq { border-left: 3px solid var(--primary); padding: 4px 12px; margin: 6px 0; background: #f8faf8; color: #666; border-radius: 0 4px 4px 0; font-size: 13px; }
|
||||
.md-list { margin: 4px 0; padding-left: 20px; font-size: 13px; line-height: 1.6; }
|
||||
.md-list li { margin: 2px 0; }
|
||||
.md-list .md-list { margin: 2px 0 2px 0; }
|
||||
.md-p { margin: 4px 0; line-height: 1.6; font-size: 13px; }
|
||||
.md-p a, .md-list a, .md-bq a { color: var(--primary); text-decoration: none; }
|
||||
.md-p a:hover, .md-list a:hover { text-decoration: underline; }
|
||||
.md-table { border-collapse: collapse; margin: 6px 0; font-size: 12px; width: 100%; }
|
||||
.md-table th { border: 1px solid #e5e5e5; padding: 4px 8px; background: #f7f8fa; font-weight: 600; text-align: left; }
|
||||
.md-table td { border: 1px solid #e5e5e5; padding: 4px 8px; }
|
||||
.md-table tr:hover td { background: rgba(7,193,96,0.02); }
|
||||
.md-img { max-width: 100%; max-height: 200px; border-radius: 6px; margin: 6px 0; display: block; object-fit: contain; cursor: pointer; }
|
||||
|
||||
/* === Agent 流式显示 === */
|
||||
.agent-stream { display: flex; flex-direction: column; gap: 0; max-width: 90%; margin-bottom: 6px; }
|
||||
.agent-tree-root {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 6px 12px; font-size: 12px; font-weight: 500; cursor: pointer;
|
||||
border-radius: 8px; background: #fff; border: 0.5px solid #e0e0e0;
|
||||
box-shadow: 0 2px 4px rgba(7,192,95,0.08); color: var(--text);
|
||||
transition: all 0.25s; margin-bottom: 8px; user-select: none;
|
||||
}
|
||||
.agent-tree-root:hover { background: rgba(7,192,95,0.04); }
|
||||
.agent-tree-root-title { display: flex; align-items: center; gap: 6px; }
|
||||
.agent-tree-root-title svg { width: 16px; height: 16px; color: var(--primary); flex-shrink: 0; }
|
||||
.agent-tree-root-toggle { font-size: 12px; color: var(--primary); transition: transform 0.2s; }
|
||||
.agent-tree-root.expanded .agent-tree-root-toggle { transform: rotate(180deg); }
|
||||
.agent-tree-children { display: none; padding-left: 12px; margin-top: -4px; margin-bottom: 8px; }
|
||||
.agent-tree-root.expanded + .agent-tree-children { display: block; }
|
||||
.agent-tree-child { position: relative; padding-left: 20px; margin-bottom: 6px; }
|
||||
.agent-tree-child::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; border-left: 1px dashed #e0e0e0; }
|
||||
.agent-tree-child:last-child::before { bottom: auto; height: 15px; }
|
||||
.agent-tree-branch { position: absolute; left: 0; top: 15px; width: 16px; border-top: 1px dashed #e0e0e0; }
|
||||
.agent-card {
|
||||
background: #fff; border-radius: 5px; border: 1px solid #e8e8e8;
|
||||
overflow: hidden; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.02);
|
||||
animation: agentFadeIn 0.25s ease-out; margin-bottom: 8px; position: relative;
|
||||
}
|
||||
.agent-card:hover { border-color: var(--primary); box-shadow: 0 1px 4px rgba(7,192,95,0.08); }
|
||||
.agent-card.pending { border-color: rgba(7,192,95,0.15); }
|
||||
.agent-card.pending::after {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: linear-gradient(120deg, transparent 0%, rgba(7,192,95,0.06) 40%, rgba(7,192,95,0.08) 55%, transparent 85%);
|
||||
transform: translateX(-100%); animation: agentShimmer 2.8s ease-in-out infinite; pointer-events: none;
|
||||
}
|
||||
.agent-card.error { border-left: 2px solid #e44; }
|
||||
.agent-card-header { display: flex; justify-content: space-between; align-items: center; padding: 5px 10px; cursor: pointer; user-select: none; transition: background-color 0.15s; }
|
||||
.agent-card-header:hover { background: rgba(7,192,95,0.03); }
|
||||
.agent-card-header.no-expand { cursor: default; }
|
||||
.agent-card-header.no-expand:hover { background: transparent; }
|
||||
.agent-card-title { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }
|
||||
.agent-card-title svg { width: 14px; height: 14px; color: var(--primary); flex-shrink: 0; }
|
||||
.agent-card-name { font-size: 12px; font-weight: 500; white-space: nowrap; color: var(--text); overflow: hidden; text-overflow: ellipsis; }
|
||||
.agent-card-summary { color: #999; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; margin-left: 4px; }
|
||||
.agent-card-toggle { font-size: 12px; color: #999; transition: transform 0.2s; padding: 0 2px; flex-shrink: 0; }
|
||||
.agent-card.expanded .agent-card-toggle { transform: rotate(180deg); }
|
||||
.agent-card-detail { display: none; padding: 6px 12px; border-top: 1px solid #f0f0f0; font-size: 12px; color: #555; line-height: 1.6; max-height: 200px; overflow-y: auto; background: #fafbfc; white-space: pre-wrap; word-break: break-word; }
|
||||
.agent-card.expanded .agent-card-detail { display: block; }
|
||||
.agent-card-info { padding: 5px 10px; border-top: 1px solid #f0f0f0; font-size: 11px; font-weight: 500; color: var(--text); }
|
||||
.agent-card-info strong { color: var(--primary); font-weight: 600; }
|
||||
.agent-answer { animation: agentFadeIn 0.25s ease-out; }
|
||||
.agent-answer-content { font-size: 13px; color: #333; line-height: 1.6; background: #f7f8fa; padding: 10px 14px; border-radius: 12px; border-bottom-left-radius: 4px; }
|
||||
.agent-answer-toolbar { display: flex; gap: 6px; margin-top: 6px; }
|
||||
.agent-answer-toolbar button { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: 1px solid #e8e8e8; border-radius: 50%; background: #fff; cursor: pointer; color: #999; transition: all 0.2s; }
|
||||
.agent-answer-toolbar button:hover { border-color: var(--primary); color: var(--primary); }
|
||||
.agent-answer-toolbar button svg { width: 13px; height: 13px; }
|
||||
.agent-refs { background: #fff; border: 1px solid #e8e8e8; border-radius: 10px; margin-top: 8px; overflow: hidden; font-size: 12px; }
|
||||
.agent-refs-header { display: flex; align-items: center; gap: 6px; padding: 8px 12px; cursor: pointer; color: #555; font-weight: 500; user-select: none; }
|
||||
.agent-refs-header:hover { background: rgba(7,193,96,0.04); }
|
||||
.agent-refs-header svg:first-child { color: var(--primary); flex-shrink: 0; }
|
||||
.agent-refs-arrow { margin-left: auto; transition: transform 0.2s; color: #999; }
|
||||
.agent-refs.expanded .agent-refs-arrow { transform: rotate(180deg); }
|
||||
.agent-refs-list { display: none; border-top: 1px solid #f0f0f0; padding: 6px 12px 8px; max-height: 200px; overflow-y: auto; }
|
||||
.agent-refs.expanded .agent-refs-list { display: block; }
|
||||
.agent-ref-group { margin-bottom: 6px; }
|
||||
.agent-ref-group-title { display: flex; align-items: center; gap: 4px; font-weight: 500; color: var(--text); padding: 2px 0; font-size: 12px; }
|
||||
.agent-ref-group-title svg { color: var(--primary); flex-shrink: 0; }
|
||||
.agent-ref-count { color: #999; font-weight: 400; font-size: 11px; }
|
||||
.agent-ref-chunk { color: #999; padding: 2px 0 2px 18px; line-height: 1.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; font-size: 11px; }
|
||||
.agent-ref-chunk:hover { color: var(--primary); background: rgba(7,193,96,0.04); border-radius: 4px; }
|
||||
.agent-ref-idx { color: var(--primary); font-weight: 500; font-size: 10px; }
|
||||
@keyframes agentFadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
||||
@keyframes agentShimmer { 0% { transform: translateX(-90%); } 50% { transform: translateX(-5%); } 100% { transform: translateX(90%); } }
|
||||
@keyframes agentSpin { to { transform: rotate(360deg); } }
|
||||
.agent-spinner { width: 12px; height: 12px; border: 2px solid #e0e0e0; border-top-color: var(--primary); border-radius: 50%; animation: agentSpin 0.8s linear infinite; display: inline-block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- 左侧导航 -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-logo">
|
||||
<h1>WEKNORA</h1>
|
||||
<button class="sidebar-logo-btn" title="复制">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sidebar-nav">
|
||||
<div class="nav-item active">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
|
||||
知识库
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
知识搜索
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||
智能体
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="12" r="4"/><circle cx="17" cy="12" r="4"/></svg>
|
||||
共享空间
|
||||
</div>
|
||||
<div class="nav-item">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
对话
|
||||
<svg class="nav-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
</div>
|
||||
<div class="nav-item" id="nav-clips">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
|
||||
网页内容收藏
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-avatar" id="wk-user-letter">U</div>
|
||||
<div class="sidebar-user-info">
|
||||
<div class="sidebar-user-name" id="wk-user-name">加载中...</div>
|
||||
<div class="sidebar-user-email" id="wk-user-email"></div>
|
||||
</div>
|
||||
<svg class="sidebar-user-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间内容区 -->
|
||||
<div class="main-area">
|
||||
<div class="content-top">
|
||||
<div class="content-tabs">
|
||||
<div class="content-tab">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
||||
全部
|
||||
</div>
|
||||
<div class="content-tab active">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||||
我的
|
||||
</div>
|
||||
<div class="content-tab">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
|
||||
协作
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-header">
|
||||
<h2>知识库</h2>
|
||||
<button class="content-header-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="content-desc">管理和组织您的知识库,支持文档类型和问答型知识库</div>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
<!-- 空状态插图 -->
|
||||
<div class="empty-illustration">
|
||||
<svg width="140" height="100" viewBox="0 0 140 100">
|
||||
<!-- 箭头 -->
|
||||
<circle cx="50" cy="8" r="5" fill="#c8e6c9"/>
|
||||
<circle cx="70" cy="4" r="4" fill="#e0e0e0"/>
|
||||
<circle cx="90" cy="10" r="6" fill="#a5d6a7"/>
|
||||
<path d="M55 20 L50 30 M70 18 L70 30 M85 22 L82 30" stroke="#07C160" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
<polygon points="47,30 53,30 50,36" fill="#07C160"/>
|
||||
<polygon points="67,30 73,30 70,36" fill="#07C160"/>
|
||||
<polygon points="79,30 85,30 82,36" fill="#07C160"/>
|
||||
<!-- 盒子 -->
|
||||
<rect x="15" y="45" width="110" height="50" rx="8" fill="url(#boxGrad)"/>
|
||||
<defs><linearGradient id="boxGrad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#e8f8ee"/><stop offset="100%" stop-color="#d0f0db"/></linearGradient></defs>
|
||||
<circle cx="50" cy="72" r="5" fill="#2b6bff"/>
|
||||
<circle cx="65" cy="72" r="5" fill="#2b6bff"/>
|
||||
<circle cx="80" cy="72" r="5" fill="#e0e0e0"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="empty-title">暂无知识库</div>
|
||||
<div class="empty-desc">点击左侧快捷操作"新建知识库"按钮创建第一个知识库</div>
|
||||
<button class="empty-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
|
||||
新建知识库
|
||||
</button>
|
||||
<button class="fab-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧对话面板 -->
|
||||
<div class="chat-panel">
|
||||
<div class="chat-panel-header">
|
||||
<span class="chat-panel-icon">🐱</span>
|
||||
<span class="chat-panel-title">WeKnora Assistant</span>
|
||||
<div class="chat-panel-actions">
|
||||
<button class="chat-panel-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></button>
|
||||
<button class="chat-panel-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-panel-brand">
|
||||
<div class="chat-panel-brand-name">WeKnora</div>
|
||||
<div class="chat-panel-brand-bar">
|
||||
<button class="brand-bar-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
|
||||
<button class="brand-bar-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
|
||||
<button class="brand-bar-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-panel-body">
|
||||
<h3>开始对话</h3>
|
||||
<p>选择知识库后,输入问题开始智能问答</p>
|
||||
</div>
|
||||
<div class="chat-panel-input">
|
||||
<div class="wk-image-previews" id="wk-image-previews" style="display:none;"></div>
|
||||
<div class="chat-input-card">
|
||||
<textarea placeholder="输入问题..." rows="1"></textarea>
|
||||
<div class="chat-input-toolbar">
|
||||
<button class="wk-image-upload-btn" id="wk-image-btn" style="display:none;" title="上传图片">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
</button>
|
||||
<button class="chat-tool-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
普通
|
||||
</button>
|
||||
<button class="chat-tool-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
||||
</button>
|
||||
<button class="chat-tool-btn mode-active">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" style="width:8px;height:8px"><circle cx="12" cy="12" r="12"/></svg>
|
||||
知识库
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:10px;height:10px"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<button class="chat-tool-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9"/></svg>
|
||||
yueyue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="wk-image-input" accept="image/jpeg,image/png,image/gif,image/webp" multiple style="display:none;">
|
||||
</div>
|
||||
|
||||
<script src="weknora.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user