WIP: feature/devstar-vscode #40

Draft
lat5211 wants to merge 3 commits from feature/devstar-vscode into main
4 changed files with 500 additions and 264 deletions

View File

@@ -1080,6 +1080,8 @@ visibility.private = Private
visibility.private_tooltip = Visible only to members of organizations you have joined
[repo]
create_from_template.clear_filters = Clear Filters
create_from_template.quick_filter = Quick Filter
dev_container = Dev Container
dev_container_empty = Oops, it looks like there is no Dev Container Setting in this repository.
dev_container_invalid_config_prompt = Invalid Dev Container Configuration: Please upload a valid 'devcontainer.json' file to the default branch, and ensure that this repository is NOT archived.
@@ -1138,6 +1140,7 @@ generate_from = Generate From
repo_desc = Description
repo_desc_helper = Enter short description (optional)
repo_no_desc = No description provided
repo_no_results= No repositories found
repo_lang = Languages
repo_gitignore_helper = Select .gitignore templates.
repo_gitignore_helper_desc = Choose which files not to track from a list of templates for common languages. Typical artifacts generated by each language's build tools are included on .gitignore by default.
@@ -4158,4 +4161,4 @@ validation_error = Configuration validation error
required_field = Required field
invalid_value = Invalid value
out_of_range = Out of range
invalid_format = Invalid format
invalid_format = Invalid format

View File

@@ -772,6 +772,10 @@ update_language_not_found=语言「%s」不可用。
update_language_success=语言已更新。
update_profile_success=您的资料信息已经更新
change_username=您的用户名已更改。
[repo]
create_from_template.clear_filters=清除筛选
create_from_template.quick_filter=快速筛选
change_username_prompt=注意:更改您的用户名也更改您的帐户 URL。
change_username_redirect_prompt=在其他用户使用您的旧用户名注册前,此旧用户名将会重定向到您的新用户名
continue=继续操作
@@ -1131,6 +1135,7 @@ generate_from=生成自
repo_desc=描述
repo_desc_helper=输入简要描述 (可选)
repo_no_desc=无详细信息
repo_no_results=没有找到匹配的仓库
repo_lang=语言
repo_gitignore_helper=选择 .gitignore 模板
repo_gitignore_helper_desc=从常见语言的模板列表中选择忽略跟踪的文件。默认情况下,由开发或构建工具生成的特殊文件都包含在 .gitignore 中。
@@ -4149,4 +4154,4 @@ validation_error=配置验证错误
required_field=必填字段
invalid_value=无效值
out_of_range=超出范围
invalid_format=格式无效
invalid_format=格式无效

View File

@@ -1334,13 +1334,23 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, doer *user.User, repo *gite
}
}
// 确定完整的工作目录路径
var fullWorkPath string
if configurationModel.WorkspaceFolder != "" {
// 优先使用配置文件中的 workspaceFolder
fullWorkPath = configurationModel.WorkspaceFolder
} else {
// 否则使用 DevcontainerWorkDir + 仓库名称
fullWorkPath = devContainerInfo.DevcontainerWorkDir + "/" + repo.Repository.Name
}
// 构建并返回 URL
url := "://mengning.devstar/" +
"openProject?host=" + repo.Repository.Name +
"&hostname=" + devContainerInfo.DevcontainerHost +
"&port=" + port +
"&username=" + doer.Name +
"&path=" + devContainerInfo.DevcontainerWorkDir +
"&path=" + fullWorkPath +
"&access_token=" + access_token +
"&devstar_username=" + repo.Repository.OwnerName +
"&devstar_domain=" + cfg.Section("server").Key("ROOT_URL").Value()

View File

@@ -1,307 +1,525 @@
{{template "base/head" .}}
<!-- 强制加载 jQuery 和 Semantic UI -->
<script>
if (typeof jQuery === 'undefined') {
document.write('<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.4/dist/jquery.min.js"><\/script>');
document.write('<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.5.0/dist/semantic.min.js"><\/script>');
}
</script>
<style>
.search-sort-container {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.search-input-container {
flex: 1;
min-width: 0;
}
.sort-dropdown-container {
white-space: nowrap;
flex-shrink: 0;
}
.sort-dropdown-container:hover {
cursor: pointer;
background: var(--color-hover) !important;
}
.sort-dropdown-container.active {
background: var(--color-active) !important;
}
.sort-dropdown-trigger {
padding: 10px 12px;
}
.ui.dropdown .menu {
position: absolute;
top: 110%;
}
.ui.dropdown .text {
display: inline-block;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
/* 新增样式:选中项高亮 */
.ui.dropdown .menu .active.item,
.ui.dropdown .menu .item:hover {
background: var(--color-hover) !important;
}
/* 当前选中项样式 */
.ui.dropdown .menu .selected.item {
color: var(--color-text) !important;
background: var(--color-active) !important;
font-weight: var(--font-weight-medium) !important;
}
</style>
<div role="main" aria-label="{{.Title}}" class="page-content repository new migrate">
<div class="ui middle very relaxed page grid">
<div class="column">
<!-- 搜索和排序控制栏 -->
<div class="search-sort-container">
<!-- 搜索框 -->
<div class="ui action input search-input-container">
<input type="text" id="searchInput" placeholder="搜索模板...">
<button class="ui small icon button" id="searchButton" aria-label="{{ctx.Locale.Tr " search.search"}}" {{with
.Tooltip}}data-tooltip-content="{{.}}" {{end}}{{if .Disabled}} disabled{{end}}>{{svg
"octicon-search"}}</button>
</div>
<!-- 排序下拉框 -->
<div class="sort-dropdown-container">
<div class="ui small dropdown type jump item" id="sortDropdown">
<div class="sort-dropdown-trigger">
<span class="text">
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
</div>
<div class="menu">
<div class="item " data-value="newest">最新创建</div>
<div class="item" data-value="oldest">最早创建</div>
<div class="item" data-value="name_asc">按字母顺序排序</div>
<div class="item" data-value="name_desc">按字母逆序排序</div>
<div class="item" data-value="recently_updated">最近更新</div>
<div class="item" data-value="least_recently_updated">最早更新</div>
<div class="item" data-value="most_likes">点赞由多到少</div>
<div class="item" data-value="least_likes">点赞由少到多</div>
<div class="item" data-value="most_forks">派生由多到少</div>
<div class="item" data-value="least_forks">派生由少到多</div>
</div>
<div class="ui small secondary filter menu tw-items-center tw-mx-0 tw-mb-4">
<form class="ui form ignore-dirty tw-flex-1">
<div class="ui small fluid action input">
<input type="text" id="searchInput" name="q" placeholder="{{ctx.Locale.Tr "search.type_tooltip"}}">
<button class="ui small icon button" id="searchButton" type="submit" aria-label="{{ctx.Locale.Tr "explore.search"}}">
{{svg "octicon-search"}}
</button>
</div>
</form>
<!-- Sort -->
<div class="ui small dropdown type jump item tw-mr-0" id="sortDropdown">
<span class="text">
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<a class="item" data-value="newest">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
<a class="item" data-value="oldest">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
<a class="item" data-value="alphabetically">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
<a class="item" data-value="reversealphabetically">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
<a class="item" data-value="recentupdate">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
<a class="item" data-value="leastupdate">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
<a class="item" data-value="moststars">{{ctx.Locale.Tr "repo.issues.filter_sort.moststars"}}</a>
<a class="item" data-value="feweststars">{{ctx.Locale.Tr "repo.issues.filter_sort.feweststars"}}</a>
<a class="item" data-value="mostforks">{{ctx.Locale.Tr "repo.issues.filter_sort.mostforks"}}</a>
<a class="item" data-value="fewestforks">{{ctx.Locale.Tr "repo.issues.filter_sort.fewestforks"}}</a>
</div>
</div>
</div>
<!-- 标签筛选区域 -->
<div id="tag-filter-container" style="display: none; margin-bottom: 1rem;">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="flex: 1;">
<div class="ui small labels" id="selected-tags" style="margin: 0;"></div>
</div>
<a class="ui small basic button" id="clear-tags" style="display: none; margin-left: 1rem;">
{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.create_from_template.clear_filters"}}
</a>
</div>
</div>
<!-- 所有可用标签 -->
<div class="tw-mb-4" id="all-tags-container" style="display: none;">
<div class="tw-text-12 tw-text-grey tw-mb-2">{{ctx.Locale.Tr "repo.create_from_template.quick_filter"}}:</div>
<div class="ui small labels tw-flex-wrap" id="all-tags"></div>
</div>
<div class="divider"></div>
<!-- 模板卡片容器 -->
<div class="ui cards migrate-entries" id="template-cards">
<div class="ui active inverted">
<div class="ui text loader">加载模板中...</div>
</div>
<div class="ui active centered inline text loader">{{ctx.Locale.Tr "loading"}}</div>
</div>
</div>
</div>
</div>
<script>
// 确保 jQuery 已加载
function ensureJQuery(callback) {
if (window.jQuery) {
callback();
} else {
setTimeout(() => ensureJQuery(callback), 100);
(function() {
'use strict';
const appSubUrl = '{{.AppSubUrl}}';
const i18n = {
errorOccurred: '{{ctx.Locale.Tr "error.occurred"}}',
noResults: '{{ctx.Locale.Tr "repo.repo_no_results"}}',
loading: '{{ctx.Locale.Tr "loading"}}'
};
// 标签类型配置(支持前缀约定,如 os:linux, framework:react
const tagTypeConfig = {
'os': { color: 'blue', label: '系统' },
Review

这里的os应该是开发环境操作系统,可以考虑叫devos开发环境OS,类似devos:ubuntu-latest

docker search ubuntu
docker search alpine
docker search centos
docker search openeuler/openeuler
docker search debian
docker search fedora

主要有这些,每种还有不同的版本,怎么形成开发环境OS的下拉列表项目?合并所有devos:类?默认植入一些主流的版本?

这里的os应该是开发环境操作系统,可以考虑叫devos开发环境OS,类似devos:ubuntu-latest docker search ubuntu docker search alpine docker search centos docker search openeuler/openeuler docker search debian docker search fedora 主要有这些,每种还有不同的版本,怎么形成开发环境OS的下拉列表项目?合并所有devos:类?默认植入一些主流的版本?
'platform': { color: 'purple', label: '平台' },
'arch': { color: 'purple', label: '架构' },
'framework': { color: 'teal', label: '框架' },
'lang': { color: 'orange', label: '语言' },
'type': { color: 'green', label: '类型' },
'tool': { color: 'brown', label: '工具' },
'default': { color: 'grey', label: '' }
};
// 解析标签(支持 prefix:value 格式)
function parseTag(tag) {
const match = tag.match(/^([a-z]+):(.+)$/);
if (match) {
const [, prefix, value] = match;
const config = tagTypeConfig[prefix] || tagTypeConfig.default;
return {
prefix: prefix,
value: value,
display: value,
original: tag,
type: prefix,
color: config.color,
typeLabel: config.label
};
}
// 无前缀的普通标签
return {
prefix: null,
value: tag,
display: tag,
original: tag,
type: 'topic',
color: 'grey',
typeLabel: ''
};
}
document.addEventListener('DOMContentLoaded', function () {
ensureJQuery(function () {
// 初始化变量
const cardsContainer = document.getElementById('template-cards');
const searchInput = document.getElementById('searchInput');
const searchButton = document.getElementById('searchButton');
const sortContainer = document.querySelector('.sort-dropdown-container');
const sortTrigger = document.querySelector('.sort-dropdown-trigger');
const sortDropdown = $('#sortDropdown');
let allRepos = [];
let currentSearchTerm = '';
let currentSortValue = 'newest';
// 根据标签内容智能选择颜色
function getSmartColor(tag) {
const lower = tag.toLowerCase();
// 常见语言
if (['javascript', 'js', 'typescript', 'ts'].includes(lower)) return 'yellow';
if (['python', 'py'].includes(lower)) return 'blue';
if (['java'].includes(lower)) return 'red';
if (['go', 'golang'].includes(lower)) return 'teal';
if (['rust'].includes(lower)) return 'orange';
if (['c++', 'cpp', 'c'].includes(lower)) return 'purple';
// 常见框架
if (['react', 'reactjs'].includes(lower)) return 'blue';
if (['vue', 'vuejs'].includes(lower)) return 'green';
if (['angular'].includes(lower)) return 'red';
if (['django'].includes(lower)) return 'green';
if (['spring', 'springboot'].includes(lower)) return 'green';
// 操作系统
if (['linux', 'ubuntu', 'debian'].includes(lower)) return 'orange';
if (['windows'].includes(lower)) return 'blue';
if (['macos', 'mac'].includes(lower)) return 'grey';
return 'grey';
}
// 全局设置silent模式
$.fn.dropdown.settings.silent = true;
$.fn.transition.settings.silent = true;
function initPage() {
const cardsContainer = document.getElementById('template-cards');
const searchInput = document.getElementById('searchInput');
const searchButton = document.getElementById('searchButton');
const allTagsContainer = document.getElementById('all-tags-container');
const allTagsElement = document.getElementById('all-tags');
const selectedTagsElement = document.getElementById('selected-tags');
const tagFilterContainer = document.getElementById('tag-filter-container');
const clearTagsButton = document.getElementById('clear-tags');
// 初始化下拉框并添加选中状态处理
sortDropdown.dropdown({
action: 'hide',
onShow: function () {
$(`.ui.dropdown .menu .item[data-value="${currentSortValue}"]`).addClass('selected');
return document.body.contains(this);
},
onHide: function () {
return document.body.contains(this);
},
onChange: function (value) {
currentSortValue = value;
$('.ui.dropdown .menu .item').removeClass('selected');
$(`.ui.dropdown .menu .item[data-value="${value}"]`).addClass('selected');
sortContainer.classList.remove('active');
applyFiltersAndRender();
},
onInitialize: function () {
$('.ui.dropdown .menu .item').removeClass('selected');
$(`.ui.dropdown .menu .item[data-value="newest"]`).addClass('selected');
}
});
if (typeof window.$ === 'undefined') {
setTimeout(initPage, 100);
return;
}
// 点击触发器时切换 active 类
sortTrigger.addEventListener('click', function () {
sortContainer.classList.toggle('active');
$(`.ui.dropdown .menu .item[data-value="${currentSortValue}"]`).addClass('selected');
});
const $ = window.$;
let allRepos = [];
let currentSearchTerm = '';
let currentSortValue = 'newest';
let selectedTags = new Set();
let allAvailableTags = new Set();
// 点击页面其他位置时移除 active 类
document.addEventListener('click', function (event) {
if (!sortContainer.contains(event.target)) {
sortContainer.classList.remove('active');
}
});
//鼠标右键时移除active类
document.addEventListener('contextmenu', function (event) {
if (!sortContainer.contains(event.target)) {
sortContainer.classList.remove('active');
}
});
$('#sortDropdown').dropdown({
action: 'hide',
onChange: function(value) {
currentSortValue = value;
applyFiltersAndRender();
}
});
// 获取模板数据
fetch('https://devstar.cn/api/v1/repos/search?template=true')
.then(response => response.json())
.then(data => {
allRepos = (data.data || []).map(repo => ({
...repo,
fetch('https://devstar.cn/api/v1/repos/search?template=true')
.then(response => response.json())
.then(data => {
allRepos = (data.data || []).map(repo => {
// 提取标签:语言 + topics
const tags = [];
if (repo.language) tags.push(repo.language);
if (repo.topics && Array.isArray(repo.topics)) {
tags.push(...repo.topics);
}
// 收集所有标签
tags.forEach(tag => allAvailableTags.add(tag));
return {
name: repo.name,
description: repo.description || '',
clone_url: repo.clone_url,
full_name: "Devstar.cn/" + repo.full_name,
language: repo.language || '',
topics: repo.topics || [],
tags: tags,
createdTimestamp: new Date(repo.created_at).getTime(),
updatedTimestamp: new Date(repo.updated_at).getTime(),
lowerName: repo.name.toLowerCase(),
lowerDescription: (repo.description || '').toLowerCase(),
starsCount: repo.stars_count || 0,
forksCount: repo.forks_count || 0
}));
applyFiltersAndRender();
// 搜索按钮点击事件
searchButton.addEventListener('click', function () {
currentSearchTerm = searchInput.value.trim().toLowerCase();
applyFiltersAndRender();
});
// 输入框回车事件
searchInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
currentSearchTerm = searchInput.value.trim().toLowerCase();
applyFiltersAndRender();
}
});
})
.catch(error => {
console.error('获取模板数据失败:', error);
cardsContainer.innerHTML = '<div class="ui error message">加载模板失败</div>';
};
});
// 应用搜索和排序筛选
function applyFiltersAndRender() {
// 1. 应用搜索筛选
let filteredRepos = currentSearchTerm ?
allRepos.filter(repo =>
repo.lowerName.includes(currentSearchTerm)
) :
[...allRepos];
renderAllTags();
applyFiltersAndRender();
// 2. 应用排序
let sortedRepos = [...filteredRepos];
switch (currentSortValue) {
case 'newest':
sortedRepos.sort((a, b) => b.createdTimestamp - a.createdTimestamp);
break;
case 'oldest':
sortedRepos.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
break;
case 'name_asc':
sortedRepos.sort((a, b) => a.name.localeCompare(b.name));
break;
case 'name_desc':
sortedRepos.sort((a, b) => b.name.localeCompare(a.name));
break;
case 'recently_updated':
sortedRepos.sort((a, b) => b.updatedTimestamp - a.updatedTimestamp);
break;
case 'least_recently_updated':
sortedRepos.sort((a, b) => a.updatedTimestamp - b.updatedTimestamp);
break;
case 'most_likes':
sortedRepos.sort((a, b) => b.starsCount - a.starsCount);
break;
case 'least_likes':
sortedRepos.sort((a, b) => a.starsCount - b.starsCount);
break;
case 'most_forks':
sortedRepos.sort((a, b) => b.forksCount - a.forksCount);
break;
case 'least_forks':
sortedRepos.sort((a, b) => a.forksCount - b.forksCount);
break;
searchButton.addEventListener('click', function (e) {
e.preventDefault();
currentSearchTerm = searchInput.value.trim().toLowerCase();
applyFiltersAndRender();
});
searchInput.addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
currentSearchTerm = searchInput.value.trim().toLowerCase();
applyFiltersAndRender();
}
});
clearTagsButton.addEventListener('click', function(e) {
e.preventDefault();
selectedTags.clear();
updateSelectedTagsDisplay();
applyFiltersAndRender();
});
})
.catch(error => {
console.error('Error:', error);
cardsContainer.innerHTML = '<div class="ui error message">' + i18n.errorOccurred + '</div>';
});
function renderAllTags() {
if (allAvailableTags.size === 0) return;
allTagsContainer.style.display = 'block';
allTagsElement.innerHTML = '';
// 按标签类型分组
const tagsByType = {};
Array.from(allAvailableTags).forEach(tag => {
const parsed = parseTag(tag);
const type = parsed.type || 'topic';
if (!tagsByType[type]) tagsByType[type] = [];
tagsByType[type].push(parsed);
});
// 按类型渲染标签
const typeOrder = ['os', 'platform', 'arch', 'framework', 'lang', 'type', 'tool', 'topic'];
typeOrder.forEach(type => {
if (!tagsByType[type]) return;
// 添加类型标题
if (type !== 'topic' && tagsByType[type].length > 0) {
const config = tagTypeConfig[type] || tagTypeConfig.default;
const typeLabel = document.createElement('span');
typeLabel.className = 'tw-text-12 tw-text-grey tw-mr-2';
typeLabel.textContent = config.label + ':';
allTagsElement.appendChild(typeLabel);
}
// 3. 渲染结果
renderTemplates(sortedRepos);
// 渲染该类型的标签
tagsByType[type].sort((a, b) => a.display.localeCompare(b.display)).forEach(parsed => {
const label = document.createElement('a');
const color = parsed.color || getSmartColor(parsed.value);
const isSelected = selectedTags.has(parsed.original);
// 确保颜色类正确应用,选中时添加 basic 样式
label.className = 'ui ' + color + ' label' + (isSelected ? ' basic' : '');
label.textContent = parsed.display;
label.style.cursor = 'pointer';
label.style.marginBottom = '4px';
label.style.marginRight = '4px';
label.style.transition = 'all 0.2s';
// 添加类型提示
if (parsed.typeLabel) {
label.setAttribute('data-tooltip-content', parsed.typeLabel + ': ' + parsed.display);
}
// 悬停效果
label.addEventListener('mouseenter', function() {
if (!selectedTags.has(parsed.original)) {
this.style.transform = 'scale(1.05)';
this.style.boxShadow = '0 2px 4px rgba(0,0,0,0.15)';
}
});
label.addEventListener('mouseleave', function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = 'none';
});
label.addEventListener('click', function(e) {
e.preventDefault();
toggleTag(parsed.original);
});
allTagsElement.appendChild(label);
});
// 添加分隔(除了最后一个类型)
if (type !== 'topic' && tagsByType[type].length > 0) {
const separator = document.createElement('div');
separator.className = 'tw-w-full tw-h-2';
allTagsElement.appendChild(separator);
}
});
}
function toggleTag(tag) {
if (selectedTags.has(tag)) {
selectedTags.delete(tag);
} else {
selectedTags.add(tag);
}
updateSelectedTagsDisplay();
applyFiltersAndRender();
}
function updateSelectedTagsDisplay() {
if (selectedTags.size === 0) {
tagFilterContainer.style.display = 'none';
clearTagsButton.style.display = 'none';
return;
}
// 渲染模板列表
function renderTemplates(repos) {
cardsContainer.innerHTML = '';
tagFilterContainer.style.display = 'flex';
clearTagsButton.style.display = 'block';
selectedTagsElement.innerHTML = '';
if (repos && repos.length > 0) {
repos.forEach(repo => {
const createdDate = new Date(repo.owner?.created || repo.created_at);
const formattedDate = createdDate.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
selectedTags.forEach(tag => {
const parsed = parseTag(tag);
const color = parsed.color || getSmartColor(parsed.value);
const label = document.createElement('div');
// 确保颜色类正确应用
label.className = 'ui ' + color + ' label';
label.style.paddingRight = '2.5em';
label.style.position = 'relative';
label.style.marginRight = '0.5em';
label.style.marginBottom = '0.5em';
// 标签文本
const text = document.createElement('span');
text.textContent = parsed.display;
label.appendChild(text);
// 删除按钮(使用 × 字符)
const deleteBtn = document.createElement('span');
deleteBtn.innerHTML = '×';
deleteBtn.style.position = 'absolute';
deleteBtn.style.right = '0.5em';
deleteBtn.style.top = '50%';
deleteBtn.style.transform = 'translateY(-50%)';
deleteBtn.style.cursor = 'pointer';
deleteBtn.style.fontSize = '1.2em';
deleteBtn.style.fontWeight = 'bold';
deleteBtn.style.opacity = '0.6';
deleteBtn.style.transition = 'all 0.2s';
deleteBtn.style.lineHeight = '1';
deleteBtn.style.width = '1em';
deleteBtn.style.height = '1em';
deleteBtn.style.display = 'flex';
deleteBtn.style.alignItems = 'center';
deleteBtn.style.justifyContent = 'center';
deleteBtn.setAttribute('title', '点击移除筛选条件');
deleteBtn.addEventListener('mouseenter', function() {
this.style.opacity = '1';
this.style.transform = 'translateY(-50%) scale(1.3)';
});
deleteBtn.addEventListener('mouseleave', function() {
this.style.opacity = '0.6';
this.style.transform = 'translateY(-50%) scale(1)';
});
deleteBtn.addEventListener('click', function(e) {
e.stopPropagation();
toggleTag(tag);
});
label.appendChild(deleteBtn);
selectedTagsElement.appendChild(label);
});
}
function applyFiltersAndRender() {
let filteredRepos = allRepos.slice();
// 按标签筛选
if (selectedTags.size > 0) {
filteredRepos = filteredRepos.filter(repo => {
return Array.from(selectedTags).every(tag => repo.tags.includes(tag));
});
}
// 按搜索词筛选
if (currentSearchTerm) {
filteredRepos = filteredRepos.filter(repo =>
repo.lowerName.includes(currentSearchTerm)
);
}
// 排序
let sortedRepos = filteredRepos.slice();
switch (currentSortValue) {
case 'newest':
sortedRepos.sort((a, b) => b.createdTimestamp - a.createdTimestamp);
break;
case 'oldest':
sortedRepos.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
break;
case 'alphabetically':
sortedRepos.sort((a, b) => a.name.localeCompare(b.name));
break;
case 'reversealphabetically':
sortedRepos.sort((a, b) => b.name.localeCompare(a.name));
break;
case 'recentupdate':
sortedRepos.sort((a, b) => b.updatedTimestamp - a.updatedTimestamp);
break;
case 'leastupdate':
sortedRepos.sort((a, b) => a.updatedTimestamp - b.updatedTimestamp);
break;
case 'moststars':
sortedRepos.sort((a, b) => b.starsCount - a.starsCount);
break;
case 'feweststars':
sortedRepos.sort((a, b) => a.starsCount - b.starsCount);
break;
case 'mostforks':
sortedRepos.sort((a, b) => b.forksCount - a.forksCount);
break;
case 'fewestforks':
sortedRepos.sort((a, b) => a.forksCount - b.forksCount);
break;
}
renderTemplates(sortedRepos);
}
function renderTemplates(repos) {
cardsContainer.innerHTML = '';
if (repos && repos.length > 0) {
repos.forEach(repo => {
const card = document.createElement('a');
card.className = 'ui card migrate-entry';
card.href = appSubUrl + '/repo/create?template_name=' + encodeURIComponent(repo.name) +
'&description=' + encodeURIComponent(repo.clone_url) +
'&template_full_name=' + encodeURIComponent(repo.full_name);
const imageContainer = document.createElement('div');
imageContainer.className = 'tw-flex tw-justify-center tw-p-4';
const img = document.createElement('img');
img.src = '/assets/img/favicon.svg';
img.width = 120;
img.height = 120;
img.alt = repo.name;
imageContainer.appendChild(img);
const content = document.createElement('div');
content.className = 'content';
const header = document.createElement('div');
header.className = 'header tw-text-center';
header.textContent = repo.name;
const desc = document.createElement('div');
desc.className = 'description tw-text-center tw-text-balance tw-mb-2';
desc.textContent = repo.description;
// 添加标签
if (repo.tags.length > 0) {
const tagsContainer = document.createElement('div');
tagsContainer.className = 'tw-flex tw-justify-center tw-flex-wrap tw-gap-1 tw-mt-2';
repo.tags.slice(0, 6).forEach(tag => {
const parsed = parseTag(tag);
const tagLabel = document.createElement('span');
const color = parsed.color || getSmartColor(parsed.value);
// 确保颜色类在 label 之前
tagLabel.className = 'ui mini ' + color + ' label';
// 如果有类型前缀,显示类型标签
if (parsed.typeLabel) {
tagLabel.innerHTML = '<small>' + parsed.typeLabel + '</small> ' + parsed.display;
} else {
tagLabel.textContent = parsed.display;
}
tagLabel.style.cursor = 'pointer';
tagLabel.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
toggleTag(tag);
});
tagsContainer.appendChild(tagLabel);
});
const card = `
<div class="ui card migrate-entry">
<a class="ui card migrate-entry tw-flex tw-items-center"
href="{{.AppSubUrl}}/repo/create?template_name=${repo.name}&description=${repo.clone_url}&template_full_name=${repo.full_name}">
<img src="/assets/img/favicon.svg" width="184" height="184" class="tw-p-4" alt="${repo.name}" />
<div class="content">
<div class="header tw-text-center">${repo.name}</div>
<div class="description tw-text-center tw-text-balance">${repo.description}</div>
</div>
</a>
</div>
`;
cardsContainer.insertAdjacentHTML('beforeend', card);
});
} else {
cardsContainer.innerHTML = '<div class="ui warning message">未找到匹配的模板</div>';
}
if (repo.tags.length > 6) {
const moreLabel = document.createElement('span');
moreLabel.className = 'ui mini label grey';
moreLabel.textContent = '+' + (repo.tags.length - 6);
tagsContainer.appendChild(moreLabel);
}
// 确保下拉框状态正确
sortDropdown.dropdown('refresh');
content.appendChild(header);
content.appendChild(desc);
content.appendChild(tagsContainer);
} else {
content.appendChild(header);
content.appendChild(desc);
}
card.appendChild(imageContainer);
card.appendChild(content);
cardsContainer.appendChild(card);
});
} else {
const msg = i18n.noResults;
cardsContainer.innerHTML = '<div class="ui warning message">' + msg + '</div>';
}
});
});
}
}
window.addEventListener('load', initPage);
})();
</script>
{{template "base/footer" .}}