Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions eslint.config.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESLint 配置不需要手动加这么多 globals,只需要这样即可:

languageOptions: {
      globals: {
        ...globals.browser,
      },
    },

需要安装 globals 作为依赖

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export default ts.config(
ts.configs.strictTypeChecked,
ts.configs.stylisticTypeChecked,
vue.configs["flat/recommended"],
{
rules: {
"vue/no-v-html": "off", // Disable v-html warning as we need it for formatted text
},
},
{
// FIXME: remove this once we no longer need the bracket fix
ignores: ["**/*.js"],
Expand All @@ -19,6 +24,19 @@ export default ts.config(
parser: ts.parser,
projectService: true,
},
globals: {
window: "readonly",
document: "readonly",
navigator: "readonly",
setTimeout: "readonly",
console: "readonly",
alert: "readonly",
fetch: "readonly",
ClipboardItem: "readonly",
},
// Add browser environment
ecmaVersion: 2022,
sourceType: "module",
},
},
prettier,
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
},
"dependencies": {
"@primeuix/themes": "^1.0.3",
"primevue": "^4.3.3"
"dom-to-image-more": "^3.5.0",
"primevue": "^4.3.3",
"qrcodejs2": "^0.0.2"
},
"devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.1",
Expand Down
8 changes: 8 additions & 0 deletions src/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ import DefaultTheme from "vitepress/theme";
import Aura from "@primeuix/themes/aura";
import PrimeVue from "primevue/config";


// Import components
import AppLayout from "@/components/AppLayout.vue";
import SelectionShareMenu from "@/components/SelectionShareMenu.vue";
import PostList from "@/components/PostList.vue";
import redirect from "@/redirect";

import "./style.css";

// Define the theme
export default {
extends: DefaultTheme,
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
Layout: AppLayout as any,
enhanceApp({ app, router }) {
/* client side redirections */
router.onBeforeRouteChange = (to) => redirect(to, router);
Expand All @@ -21,5 +28,6 @@ export default {

/* register global components */
app.component("PostList", PostList);
app.component("SelectionShareMenu", SelectionShareMenu);
},
} as Theme;
15 changes: 15 additions & 0 deletions src/components/AppLayout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
import DefaultTheme from "vitepress/theme";

import SelectionShareMenu from "./SelectionShareMenu.vue";

const { Layout: DefaultLayout } = DefaultTheme;
</script>

<template>
<DefaultLayout>
<template #layout-bottom>
<SelectionShareMenu />
</template>
</DefaultLayout>
</template>
199 changes: 199 additions & 0 deletions src/components/SelectionShareMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";

import ShareCard from "./ShareCard.vue";

const menuVisible = ref<boolean>(false);
const menuPosition = ref<{ x: number; y: number }>({ x: 0, y: 0 });
const selectedText = ref<string>("");
const shareCardVisible = ref<boolean>(false);

// Function to show the menu
const showMenu = (x: number, y: number, text: string) => {
if (text && text.trim() !== "") {
selectedText.value = text;
menuPosition.value = { x, y };
menuVisible.value = true;
}
};

// Function to hide the menu
const hideMenu = () => {
menuVisible.value = false;
};

// Handle text selection
const handleSelection = () => {
if (typeof window === "undefined") return;

const selection = window.getSelection();
if (selection && selection.toString().trim() !== "") {
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();

// Position the menu above the selection
const x = rect.left + rect.width / 2;
const y = rect.top - 10; // Position above the selection

// 获取选中文本的HTML内容
const container = document.createElement("div");
const clonedSelection = range.cloneContents();
container.appendChild(clonedSelection);
const htmlContent = container.innerHTML;

// 使用HTML内容而不是纯文本
showMenu(x, y, htmlContent);
}
};

// Handle mouseup event to detect selection
const handleMouseUp = () => {
// 如果分享卡片已显示,不再监听选择变化
if (typeof window === "undefined" || shareCardVisible.value) return;
setTimeout(handleSelection, 10); // Small delay to ensure selection is complete
};

// Copy selected text to clipboard
const copyText = async () => {
if (typeof navigator === "undefined") return;

try {
await navigator.clipboard.writeText(selectedText.value);
// Use a non-blocking notification instead of alert
console.log("文本已复制到剪贴板");
hideMenu();
} catch (err) {
console.error("复制失败:", err);
}
};

// Share selected text
const shareText = () => {
// Show share card in the current page
shareCardVisible.value = true;
hideMenu();
};

// Close share card
const closeShareCard = () => {
shareCardVisible.value = false;
// 清空选中的文本,以便用户可以重新选择
setTimeout(() => {
if (typeof window !== "undefined") {
window.getSelection()?.removeAllRanges();
}
}, 100);
};

// Click outside to hide menu
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (menuVisible.value && !target.closest(".selection-menu")) {
hideMenu();
}
};

onMounted(() => {
if (typeof document === "undefined") return;

document.addEventListener("mouseup", handleMouseUp);
document.addEventListener("mousedown", handleClickOutside);
});

onUnmounted(() => {
if (typeof document === "undefined") return;

document.removeEventListener("mouseup", handleMouseUp);
document.removeEventListener("mousedown", handleClickOutside);
});
</script>

<template>
<div>
<div
v-if="menuVisible"
class="selection-menu"
:style="{
left: `${menuPosition.x}px`,
top: `${menuPosition.y}px`,
transform: 'translate(-50%, -100%)',
}"
>
<button class="menu-button copy-button" title="复制" @click="copyText">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
></path>
</svg>
</button>
<button class="menu-button share-button" title="分享" @click="shareText">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="18" cy="5" r="3"></circle>
<circle cx="6" cy="12" r="3"></circle>
<circle cx="18" cy="19" r="3"></circle>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line>
</svg>
</button>
</div>

<!-- Share Card Component -->
<ShareCard
:text="selectedText"
:visible="shareCardVisible"
@close="closeShareCard"
/>
</div>
</template>

<style scoped>
.selection-menu {
position: fixed;
display: flex;
background-color: #333;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
z-index: 1000;
padding: 8px;
}

.menu-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
margin: 0 4px;
border: none;
border-radius: 4px;
cursor: pointer;
background-color: transparent;
color: white;
transition: background-color 0.2s;
}

.menu-button:hover {
background-color: rgba(255, 255, 255, 0.1);
}
</style>
Loading
Loading