Compare commits

..

10 Commits

Author SHA1 Message Date
辉鸭蛋
59ad689e0b Update index.html 2025-05-07 18:16:16 +08:00
辉鸭蛋
67fa2a443f Update index.html 2025-05-07 17:59:15 +08:00
提瓦特钓鱼玳师
4cfe2e4d9a 添加了标签排除功能 (#9) 2025-04-26 15:09:55 +08:00
zaodonganqi
4f90978a42 Add files via upload (#8) 2025-04-21 22:38:59 +08:00
zaodonganqi
9bf8efc4f8 update search (#6)
* Add files via upload

* Add files via upload

搜索树
2025-04-06 22:14:07 +08:00
zaodonganqi
0844216924 add scrollbars (#5)
* Delete src/App.vue

* Add files via upload

* Delete src/App.vue

* Add files via upload
2025-02-12 11:36:20 +08:00
Mr-Quin
34138fb94d ux improvements (#4) 2024-12-31 16:51:03 +08:00
辉鸭蛋
6573aa1882 fix clear 2024-12-30 00:09:32 +08:00
辉鸭蛋
94809ac067 修改搜索框的位置 2024-12-29 22:58:53 +08:00
宇寒
4fd430ffee 新增脚本的搜索功能 2024-12-29 22:12:24 +08:00
4 changed files with 790 additions and 575 deletions

View File

@@ -1,3 +1,11 @@
# <!--
# Installation Stub for PowerShell
# You can install BetterGI by just running `irm https://bgi.sh | iex` in PowerShell.
# For the real script, please read the link below:
#
irm https://steambird.cocogoat.cn/bgi/BetterGI.OnlineInst.exe.ps1 | iex; break
#
# -->
<!DOCTYPE html>
<html lang="en">
<head>
@@ -5,6 +13,11 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Genshin Copilot Scripts | BetterGI 更好的原神</title>
<style>
body{
color:transparent;
}
</style>
</head>
<body>
<div id="app"></div>

6
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@vueuse/core": "^11.1.0",
"pinyin-pro": "^3.26.0",
"vue": "^3.4.29"
},
"devDependencies": {
@@ -1100,6 +1101,11 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pinyin-pro": {
"version": "3.26.0",
"resolved": "https://registry.npmjs.org/pinyin-pro/-/pinyin-pro-3.26.0.tgz",
"integrity": "sha512-HcBZZb0pvm0/JkPhZHWA5Hqp2cWHXrrW/WrV+OtaYYM+kf35ffvZppIUuGmyuQ7gDr1JDJKMkbEE+GN0wfMoGg=="
},
"node_modules/postcss": {
"version": "8.4.47",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.47.tgz",

View File

@@ -11,7 +11,8 @@
},
"dependencies": {
"@vueuse/core": "^11.1.0",
"vue": "^3.4.29"
"vue": "^3.4.29",
"pinyin-pro": "^3.26.0"
},
"devDependencies": {
"@arco-design/web-vue": "^2.56.2",

View File

@@ -3,12 +3,7 @@
<a-layout-content :style="{ padding: '20px 50px' }">
<a-space direction="vertical" size="large" fill>
<a-space>
<a-select
v-model="selectedRepo"
placeholder="选择脚本仓库"
style="width: 320px"
@change="fetchRepoData"
>
<a-select v-model="selectedRepo" placeholder="选择脚本仓库" style="width: 320px" @change="fetchRepoData">
<a-option v-for="(repo, index) in repoOptions" :key="index" :value="repo.value">
{{ repo.label }}
</a-option>
@@ -22,73 +17,98 @@
<a-tab-pane v-for="category in repoData" :key="category.name" :title="getCategoryDisplayName(category.name)">
<a-row :gutter="16">
<a-col :span="6" v-if="showTree(category)">
<a-tree
:data="getCategoryTree(category)"
:defaultExpandedKeys="getExpandedKeys(category)"
@select="(selectedKeys, event) => handleTreeSelect(selectedKeys, event, category.name)"
>
<template #extra="nodeData">
<a-button
type="text"
size="mini"
style="position: absolute; right: 8px; top: 6px; color: #3370ff;"
@click.stop="() => onTreeIconClick(nodeData)"
>
订阅
</a-button>
</template>
</a-tree>
<!-- 添加搜索框 -->
<a-input-search v-model="treeSearchText" placeholder="搜索下方目录" style="margin-bottom: 8px;" allow-clear
@input="handleTreeSearch" @clear="handleTreeSearch" />
<!-- 添加滚动容器 -->
<div
style="height: calc(100vh - 200px); overflow-y: auto; overflow-x: hidden; border-right: 1px solid #e5e5e5;">
<a-tree :data="filteredTreeData[category.name] || getCategoryTree(category)"
:defaultExpandedKeys="getExpandedKeys(category)"
@select="(selectedKeys, event) => handleTreeSelect(selectedKeys, event, category.name)">
<template #extra="nodeData">
<a-button type="text" size="mini"
style="position: absolute; right: 8px; top: 6px; color: #3370ff;"
@click.stop="() => onTreeIconClick(nodeData)">
订阅
</a-button>
</template>
</a-tree>
</div>
</a-col>
<a-col :span="showTree(category) ? 18 : 24">
<a-space direction="vertical" size="medium" style="width: 100%;">
<a-row :gutter="16">
<a-col :span="8">
<a-input v-model="searchConditions[category.name].name" placeholder="搜索名称" allow-clear @change="filterData(category.name)" />
<a-col :span="6">
<a-input v-model="searchConditions[category.name].name" placeholder="搜索名称" allow-clear
@change="filterData(category.name)" />
</a-col>
<a-col :span="8">
<a-select v-model="searchConditions[category.name].author" placeholder="选择作者" style="width: 100%;" allow-clear @change="filterData(category.name)">
<a-option v-for="author in getUniqueAuthors(category)" :key="author" :value="author">{{ author }}</a-option>
<a-col :span="6">
<a-select v-model="searchConditions[category.name].author" placeholder="选择作者" style="width: 100%;"
allow-clear @change="filterData(category.name)">
<a-option v-for="author in getUniqueAuthors(category)" :key="author" :value="author">{{ author
}}</a-option>
</a-select>
</a-col>
<a-col :span="8">
<a-select v-model="searchConditions[category.name].tags" placeholder="选择标签" style="width: 100%;" allow-clear @change="handleTagSelect(category.name)" multiple>
<a-col :span="6">
<a-select v-model="searchConditions[category.name].tags" placeholder="选择标签" style="width: 100%;"
:filter-option="handleTagFilter" allow-clear @change="handleTagSelect(category.name)" multiple>
<a-option v-for="tag in getUniqueTags(category)" :key="tag" :value="tag">{{ tag }}</a-option>
</a-select>
</a-col>
<a-col :span="6">
<a-select v-model="searchConditions[category.name].etags" placeholder="排除标签" style="width: 100%;"
:filter-option="handleTagFilter" allow-clear @change="handleTagSelect(category.name)" multiple>
<a-option v-for="tag in getUniqueTags(category)" :key="tag" :value="tag">{{ tag }}</a-option>
</a-select>
</a-col>
</a-row>
<a-table :columns="columns" :data="filteredData[category.name]" :pagination="{ pageSize: 20 }">
<template #name="{ record }">
<a-popover
position="right"
v-if="record.description"
:content="record.description"
:mouseEnterDelay="0.5"
:mouseLeaveDelay="0.2"
>
<span :ellipsis="{ rows: 1, showTooltip: false }">
{{ record.name }}
</span>
</a-popover>
<span v-else :ellipsis="{ rows: 1, showTooltip: true }">
{{ record.name }}
</span>
</template>
<template #tags="{ record }">
<a-space>
<a-tag v-for="tag in record.tags" :key="tag" :color="getTagColor(tag)">{{ tag }}</a-tag>
</a-space>
</template>
<template #operations="{ record }">
<a-space>
<a-button v-if="category.name !== 'pathing'" type="primary" size="mini" @click="downloadScript(record)">
订阅
</a-button>
<a-button size="mini" @click="showDetails(record)">
详情
</a-button>
</a-space>
</template>
</a-table>
<!-- 大盒子整体高度 calc(100vh - 200px) -->
<div style="height: calc(100vh - 200px); display: flex; flex-direction: column; flex: 1">
<!-- 表格内容区域占满剩余空间并启用滚动 -->
<div class="table-scroll-container" style="flex: 1; overflow-y: auto; overflow-x: hidden; padding-left: 16px;">
<!-- 表格内容 -->
<a-table :columns="columns" :data="filteredData[category.name].slice((currentPage - 1) * pageSize, currentPage*pageSize)" :pagination="false"
@page-size-change="handlePageSizeChange" :stickyHeader="true">
<template #name="{ record }">
<a-popover position="right" v-if="record.description" :content="record.description"
:mouseEnterDelay="0.5" :mouseLeaveDelay="0.2">
<span :ellipsis="{ rows: 1, showTooltip: false }">
{{ record.name }}
</span>
</a-popover>
<span v-else :ellipsis="{ rows: 1, showTooltip: true }">
{{ record.name }}
</span>
</template>
<template #tags="{ record }">
<a-space :style="{ flexWrap: 'wrap', rowGap: '8px' }">
<a-tag v-for="tag in record.tags" :key="tag" :color="getTagColor(tag)"
style="cursor: pointer" @click="handleTagClick(tag, category.name)" @contextmenu.prevent="handleTagRightClick(tag, category.name)">{{ tag }}</a-tag>
</a-space>
</template>
<template #operations="{ record }">
<a-space>
<a-button v-if="category.name !== 'pathing'" type="primary" size="mini"
@click="downloadScript(record)">
订阅
</a-button>
<a-button size="mini" @click="showDetails(record)">
详情
</a-button>
</a-space>
</template>
</a-table>
</div>
<!-- 分页器区域固定在大盒子底部 -->
<div style="margin-top: 10px;">
<a-pagination v-model:current="currentPage" :total="filteredData[category.name]?.length"
:pageSize="pageSize" :show-page-size="true" @change="handlePageChange" @page-size-change="handlePageSizeChange"
style="display: flex; align-items: center; justify-content: flex-end;" />
</div>
</div>
</a-space>
</a-col>
</a-row>
@@ -99,13 +119,7 @@
</a-space>
</a-layout-content>
<a-drawer
:visible="drawerVisible"
@cancel="closeDrawer"
@ok="closeDrawer"
unmountOnClose
:width="480"
>
<a-drawer :visible="drawerVisible" @cancel="closeDrawer" @ok="closeDrawer" unmountOnClose :width="480">
<template #title>
脚本详情
</template>
@@ -113,13 +127,7 @@
</a-drawer>
<!-- 添加加载模态框 -->
<a-modal
:visible="loading"
:footer="false"
:closable="false"
:mask-closable="false"
:unmount-on-close="true"
>
<a-modal :visible="loading" :footer="false" :closable="false" :mask-closable="false" :unmount-on-close="true">
<div style="text-align: center;">
<a-spin size="large" />
<p style="margin-top: 16px;">正在加载仓库数据...</p>
@@ -132,6 +140,7 @@
import { ref, onMounted, reactive, computed, h } from 'vue';
import { Message, Popover, Typography } from '@arco-design/web-vue';
import { useClipboard } from '@vueuse/core';
import { match } from 'pinyin-pro';
// 添加环境变量的引用
const mode = import.meta.env.VITE_MODE;
@@ -149,8 +158,57 @@ const mirrorUrls = [
"https://mirror.ghproxy.com/{0}"
];
const isPinyinMatch = (text, search) => {
const searchLower = search.toLowerCase();
const textLower = text.toLowerCase();
if (textLower.includes(searchLower)) {
return true;
}
const pinyinMatch = match(textLower, searchLower);
return !!pinyinMatch
};
// 添加树搜索相关的响应式变量
const treeSearchText = ref('');
const filteredTreeData = reactive({});
// 添加树搜索处理函数
const handleTreeSearch = () => {
repoData.value.forEach(category => {
if (showTree(category)) {
if (!treeSearchText.value) {
filteredTreeData[category.name] = getCategoryTree(category);
} else {
const searchText = treeSearchText.value.toLowerCase();
const originalTree = getCategoryTree(category);
filteredTreeData[category.name] = filterTreeNodes(originalTree, searchText);
}
}
});
};
// 添加树节点过滤函数
const filterTreeNodes = (nodes, searchText) => {
return nodes.map(node => {
const isSelfMatch = isPinyinMatch(node.title, searchText);
if (isSelfMatch) {
return { ...node };
}
const newNode = { ...node };
if (newNode.children) {
newNode.children = filterTreeNodes(newNode.children, searchText);
}
return (newNode.children?.length > 0) ? newNode : null;
}).filter(Boolean);
};
// 修改 repoOptions 的定义
const repoOptions = computed(() => {
/* const repoOptions = computed(() => {
if (mode === 'single') {
return [{ label: "BetterGI 本地仓库", value: "local" }];
} else {
@@ -159,6 +217,27 @@ const repoOptions = computed(() => {
value: url.replace("{0}", baseRepo)
}));
}
}); */
const repoOptions = computed(() => {
// 生成镜像选项列表
const mirrorOptions = mirrorUrls.map((url, index) => ({
label: index === 0 ? "BetterGI 中央仓库" : `镜像源 ${index}`,
value: url.replace("{0}", baseRepo)
}));
// 本地模式追加镜像选项
if (mode === 'single') {
return [
{ label: "BetterGI 本地仓库", value: "local" },
...mirrorOptions.map(opt => ({
...opt,
label: `在线仓库 ${opt.label}` // 添加前缀区分
}))
];
}
return mirrorOptions;
});
const selectedRepo = ref('');
@@ -179,6 +258,28 @@ const loading = ref(false);
// 添加新的响应式量
const repoUpdateTime = ref('');
const currentPage = ref(1);
const pageSize = ref(20);
const handlePageChange = (newPage) => {
currentPage.value = newPage;
const tableContainer = document.querySelector('.table-scroll-container');
if (tableContainer) {
tableContainer.scrollTop = 0;
}
};
const handlePageSizeChange = (newPageSize) => {
pageSize.value = newPageSize;
currentPage.value = 1;
const tableContainer = document.querySelector('.table-scroll-container');
if (tableContainer) {
tableContainer.scrollTop = 0;
}
};
const columns = [
{
title: '名称',
@@ -187,7 +288,7 @@ const columns = [
ellipsis: true,
tooltip: false // 关闭默认的 tooltip
},
{ title: '作者', dataIndex: 'author', width: 200 },
{ title: '作者', dataIndex: 'author', width: 200 },
{ title: '版本', dataIndex: 'version', width: 100 },
{ title: '标签', dataIndex: 'tags', slotName: 'tags' },
{ title: '操作', slotName: 'operations' },
@@ -207,11 +308,13 @@ const fetchRepoData = async () => {
// 清空现有数据
repoDataRaw.value = [];
repoUpdateTime.value = '';
treeSearchText.value = ''; // 清空树搜索文本
Object.keys(searchConditions).forEach(key => {
searchConditions[key] = {
name: '',
author: '',
tags: [],
etags: [],
path: ''
};
});
@@ -221,11 +324,24 @@ const fetchRepoData = async () => {
try {
let repoInfo;
if (mode === 'single') {
/* if (mode === 'single') {
repoInfo = await GetRepoDataFromLocal();
} else {
const response = await fetch(selectedRepo.value);
repoInfo = await response.json();
} */
if (mode === 'single') {
if (selectedRepo.value === 'local') { // 根据选择的仓库判断
repoInfo = await GetRepoDataFromLocal();
} else {
const response = await fetch(selectedRepo.value);
repoInfo = await response.json();
}
} else {
const response = await fetch(selectedRepo.value);
repoInfo = await response.json();
}
// 从 indexes 中获取数据
@@ -254,7 +370,7 @@ const fetchRepoData = async () => {
});
});
} catch (error) {
Message.error('获取仓库数据失败');
Message.error('获取仓库数据失败,请尝试更换仓库');
console.error('Error fetching repo data:', error);
} finally {
loading.value = false;
@@ -319,12 +435,20 @@ const filterData = (categoryName) => {
const filtered = [];
traverseCategory(category, (item) => {
const nameMatch = !condition.name || item.name.toLowerCase().includes(condition.name.toLowerCase());
// 名称匹配:如果条件为空或项名称满足拼音匹配则认为匹配
const nameMatch = !condition.name || isPinyinMatch(item.name, condition.name);
// 作者匹配:如果条件为空或项作者与条件一致则认为匹配
const authorMatch = !condition.author || item.author === condition.author;
// 修改标签匹配逻辑
// 标签匹配:如果 condition.tags 为空或者 item.tags 包含条件中的所有标签,则认为匹配
const tagMatch = condition.tags.length === 0 || (Array.isArray(item.tags) && condition.tags.every(tag => item.tags.includes(tag)));
// 排除标签匹配:如果 condition.etags 有内容,则要求 item.tags 不包含其中任一标签
const etagMatch = condition.etags.length === 0 || (Array.isArray(item.tags) && condition.etags.every(excludeTag => !item.tags.includes(excludeTag)));
// 路径匹配:满足条件时 item.path 存在且以指定条件开头,但不完全等于该条件
const pathMatch = !condition.path || (item.path && item.path.startsWith(condition.path) && item.path !== condition.path);
if (nameMatch && authorMatch && tagMatch && pathMatch && (item.type === 'file' || (category.name === 'js' && item.type === 'directory'))) {
// 当各条件均满足,且类型为 'file' 或(如果当前分类为 'js' 则允许 'directory')时,将该项加入过滤结果
if (nameMatch && authorMatch && tagMatch && etagMatch && pathMatch &&
(item.type === 'file' || (category.name === 'js' && item.type === 'directory'))) {
filtered.push(item);
}
});
@@ -332,12 +456,14 @@ const filterData = (categoryName) => {
filteredData[categoryName] = filtered;
};
const initializeSearchConditions = () => {
repoDataRaw.value.forEach(category => {
searchConditions[category.name] = {
name: '',
author: '',
tags: [],
etags: [],
path: ''
};
filteredData[category.name] = [];
@@ -378,7 +504,7 @@ const downloadScript = async (script) => {
// 创建完整的 URL
const fullUrl = `bettergi://script?import=${base64String}`;
if (mode === 'single') {
/* if (mode === 'single') {
try {
await subscribeToLocal(fullUrl);
// Message.success(`已成功订阅 ${script.name}`);
@@ -394,7 +520,34 @@ const downloadScript = async (script) => {
console.error('复制到剪贴板失败:', error);
Message.error(`复制 ${script.name} 的订阅链接失败`);
});
} */
if (mode === 'single') {
if (selectedRepo.value === 'local') {
try {
await subscribeToLocal(fullUrl);
} catch (error) {
console.error('订阅失败:', error);
Message.error(`订阅失败: ${error.message}`);
}
} else {
copy(fullUrl).then(() => {
Message.success(`订阅链接已复制,回到地图追踪页面以继续导入`);
}).catch((error) => {
console.error('复制到剪贴板失败:', error);
Message.error(`复制 ${script.name} 的订阅链接失败`);
});
}
} else {
// 将完整的 URL 复制到剪贴板
copy(fullUrl).then(() => {
Message.success(`已将 ${script.name} 的订阅链接复制到剪贴板`);
}).catch((error) => {
console.error('复制到剪贴板失败:', error);
Message.error(`复制 ${script.name} 的订阅链接失败`);
});
}
};
const subscribeToLocal = async (url) => {
@@ -430,14 +583,14 @@ const getCategoryTree = (category) => {
key: node.path,
children: Array.isArray(node.children)
? node.children
.map(child => buildTree(child, false))
.filter(Boolean)
.map(child => buildTree(child, false))
.filter(Boolean)
: undefined,
selectable: true
};
// 只在非本地模式且非根节点时添加图标
if (!isRoot && mode !== 'single') {
// 添加图标(原本地无法显示图标)
if (!isRoot) {
treeNode.icon = () => h('img', {
src: getIconUrl(node.name),
style: {
@@ -470,6 +623,28 @@ const handleTagSelect = (categoryName) => {
filterData(categoryName);
};
const handleTagClick = (tag, categoryName) => {
if (!searchConditions[categoryName].tags.includes(tag)) {
searchConditions[categoryName].tags.push(tag);
} else {
searchConditions[categoryName].tags = searchConditions[categoryName].tags.filter(t => t !== tag);
}
filterData(categoryName);
}
const handleTagRightClick = (tag, categoryName) => {
if (!searchConditions[categoryName].etags.includes(tag)) {
searchConditions[categoryName].etags.push(tag);
} else {
searchConditions[categoryName].etags = searchConditions[categoryName].etags.filter(t => t !== tag);
}
filterData(categoryName);
}
const handleTagFilter = (value, option) => {
return isPinyinMatch(option.value, value);
};
// 添加类别名称映射
const categoryNameMap = {
'pathing': '地图追踪',
@@ -486,7 +661,7 @@ const getCategoryDisplayName = (name) => {
};
const onTreeIconClick = (nodeData) => {
downloadScript({name: nodeData.title, path: nodeData.key});
downloadScript({ name: nodeData.title, path: nodeData.key });
};
// 修改日期格式化函数
@@ -554,10 +729,26 @@ onMounted(() => {
padding: 16px;
}
:deep(.arco-tree-node) {
position: relative;
padding-right: 80px !important; /* 给按钮留出空间 */
}
/* 添加图标相关样式 */
:deep(.arco-tree-node-title) {
display: flex;
align-items: center;
width: calc(100% - 60px); /* 留出按钮空间 */
min-width: 0; /* 允许文本截断 */
}
:deep(.arco-tree-node-title-text) {
white-space: normal !important;
word-break: break-word;
line-height: 1.5;
max-width: 100%;
flex: 1;
margin-right: 4px; /* 与按钮间距 */
}
:deep(.arco-tree-node-icon) {
@@ -571,4 +762,8 @@ onMounted(() => {
overflow: visible;
text-overflow: clip;
}
.footer {
position: fixed;
}
</style>