add Loading placeholder

This commit is contained in:
Timi
2025-07-12 10:36:33 +08:00
parent bc9dd34425
commit 70111ce18b
11 changed files with 370 additions and 369 deletions

View File

@ -264,7 +264,7 @@ const mediaComponent = computed(() => {
height: 100%; height: 100%;
.tabs { .tabs {
top: calc(50px + 49px + 30px); top: calc(96px + 30px);
position: sticky; position: sticky;
z-index: 9; z-index: 9;
border-top: var(--tui-border); border-top: var(--tui-border);

View File

@ -26,8 +26,8 @@
<span class="selectable" v-text="action.refName"></span> <span class="selectable" v-text="action.refName"></span>
</div> </div>
<template v-if="action.commitList"> <template v-if="action.commitList">
<h4 v-if="showRepo || showBranch">提交记录</h4>
<div class="commits"> <div class="commits">
<h4 v-if="showRepo || showBranch" class="title">提交记录</h4>
<div <div
class="commit" class="commit"
v-for="(commit, commitIndex) in action.commitList" v-for="(commit, commitIndex) in action.commitList"
@ -103,9 +103,17 @@ const { items } = toRefs(props);
.commits { .commits {
.title {
margin: .5rem 0;
}
.commit { .commit {
display: flex; display: flex;
&:first-child {
margin-top: .5rem;
}
&:nth-last-child(n + 2) { &:nth-last-child(n + 2) {
margin-bottom: .5rem; margin-bottom: .5rem;
} }

View File

@ -1,14 +1,12 @@
<template> <template>
<div class="repository-layout" v-if="repositoryStore.value"> <div class="repository-layout" v-if="repositoryStore.value">
<t-layout class="header"> <div class="header">
<t-content> <section class="top">
<t-space class="title" align="center"> <t-space class="left" align="center">
<h3 class="title" v-text="repositoryStore.value.name"></h3> <h4 class="title" v-text="repositoryStore.value.name"></h4>
<p class="light-gray" v-text="repositoryStore.value.description"></p> <p class="description light-gray" v-text="repositoryStore.value.description"></p>
</t-space> </t-space>
</t-content> <t-space class="right" align="center">
<t-aside class="aside" width="auto">
<t-space align="center">
<t-button class="repository-list" theme="default" variant="text" @click="backRepositoryList"> <t-button class="repository-list" theme="default" variant="text" @click="backRepositoryList">
<template #icon> <template #icon>
<icon class="icon" name="LIST" /> <icon class="icon" name="LIST" />
@ -17,40 +15,40 @@
</t-button> </t-button>
<login-menu /> <login-menu />
</t-space> </t-space>
</t-aside> </section>
</t-layout> <t-head-menu class="menu" v-model="menuValue">
<t-head-menu class="menu" v-model="menuValue"> <t-menu-item
<t-menu-item :to="`/${repositoryStore.value.name}/${repositoryStore.value.defaultBranch}`"
:to="`/${repositoryStore.value.name}/${repositoryStore.value.defaultBranch}`" :value="Menu.FILE"
:value="Menu.FILE" >
> 项目文件
项目文件 </t-menu-item>
</t-menu-item> <t-menu-item
<t-menu-item :to="`/${repositoryStore.value.name}/commits`"
:to="`/${repositoryStore.value.name}/commits`" :value="Menu.COMMIT_LOG"
:value="Menu.COMMIT_LOG" >
> 更新记录
更新记录 </t-menu-item>
</t-menu-item> <t-menu-item
<t-menu-item :to="`/${repositoryStore.value.name}/issues`"
:to="`/${repositoryStore.value.name}/issues`" :value="Menu.ISSUE"
:value="Menu.ISSUE" >
> 问题反馈
问题反馈 </t-menu-item>
</t-menu-item> <t-menu-item
<t-menu-item :to="`/${repositoryStore.value.name}/merges`"
:to="`/${repositoryStore.value.name}/merges`" :value="Menu.MERGE"
:value="Menu.MERGE" >
> 合并请求
合并请求 </t-menu-item>
</t-menu-item> <t-menu-item
<t-menu-item :to="`/${repositoryStore.value.name}/releases`"
:to="`/${repositoryStore.value.name}/releases`" :value="Menu.RELEASE"
:value="Menu.RELEASE" >
> 版本发布
版本发布 </t-menu-item>
</t-menu-item> </t-head-menu>
</t-head-menu> </div>
<div class="content"> <div class="content">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<keep-alive> <keep-alive>
@ -119,11 +117,12 @@ onMounted(async () => repositoryStore.value = await RepositoryAPI.view(route.par
<style lang="less" scoped> <style lang="less" scoped>
.repository-layout { .repository-layout {
display: flex;
border-bottom: var(--tui-border); border-bottom: var(--tui-border);
flex-direction: column;
.header { .header {
top: 0; top: 0;
height: 50px;
z-index: 3; z-index: 3;
position: sticky; position: sticky;
background: rgba(231, 234, 239, .8); background: rgba(231, 234, 239, .8);
@ -131,36 +130,49 @@ onMounted(async () => repositoryStore.value = await RepositoryAPI.view(route.par
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
.title { .top {
padding-left: .5rem; display: flex;
padding: .5rem 1rem;
border-bottom: var(--tui-border);
justify-content: space-between;
.left {
.title {
margin: 0;
padding-left: .5rem;
}
.description {
margin: 0;
font-size: 13px;
}
}
} }
.aside { .menu {
display: flex;
align-items: center;
padding-right: 1rem;
.repository-list { :deep(.t-menu) {
margin: 0;
.icon {
margin-right: .5rem;
}
} }
} }
} }
.menu { .content {
top: 50px; flex: 1;
z-index: 3; display: flex;
position: sticky; min-height: 520px;
background: rgba(255, 255, 255, .8); flex-direction: column;
border-bottom: var(--tui-border);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
:deep(.t-menu) { > * {
margin: 0; flex: 1;
} }
} }
} }
@media screen and (max-width: 1200px) {
.repository-layout {
// - 底部
min-height: calc(100% - 110px);
}
}
</style> </style>

View File

@ -60,6 +60,7 @@ export type File = {
lastCommitterDate: number; lastCommitterDate: number;
children: boolean; children: boolean;
isLoading: boolean;
} }
export enum FileViewerType { export enum FileViewerType {

View File

@ -1,30 +1,23 @@
<template> <template>
<div class="repository-list"> <div class="repository-list">
<loading :showOn="isLoading" margin="1rem" />
<t-list class="items" v-if="page" :split="true"> <t-list class="items" v-if="page" :split="true">
<t-list-item class="item" v-for="item in pageResult.list" :key="item.id"> <t-list-item class="item" v-for="item in pageResult.list" :key="item.id">
<t-layout> <icon name="DUPLICATE" :scale="2" />
<t-aside class="icon" width="auto"> <div class="content">
<icon name="DUPLICATE" :scale="2" /> <div class="header">
</t-aside> <h3 class="name">
<t-content class="content"> <router-link class="link black" :to="`/${item.name}/${item.defaultBranch}`">
<t-layout> {{ item.name }}
<t-header class="header" height="auto"> </router-link>
<div class="name"> </h3>
<router-link class="link black" :to="`/${item.name}/${item.defaultBranch}`"> <div class="time light-gray"
{{ item.name }} v-text="`最近推送 ${Time.toPassedDate(item.updatedAt)}`"
</router-link> v-popup="Time.toDateTime(item.updatedAt)"
</div> ></div>
<div class="time light-gray" </div>
v-text="`最近推送 ${Time.toPassedDate(item.updatedAt)}`" <div class="description gray" v-text="item.description"></div>
v-popup="Time.toDateTime(item.updatedAt)" </div>
></div>
</t-header>
<t-content>
<span class="gray" v-text="item.description"></span>
</t-content>
</t-layout>
</t-content>
</t-layout>
</t-list-item> </t-list-item>
</t-list> </t-list>
<footer class="footer"> <footer class="footer">
@ -44,7 +37,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import RepositoryAPI from "@/api/RepositoryAPI"; import RepositoryAPI from "@/api/RepositoryAPI";
import { Repository } from "@/types/Repository"; import { Repository } from "@/types/Repository";
import { Icon, Page, PageResult, Time } from "timi-web"; import { Icon, Loading, Page, PageResult, Time } from "timi-web";
const page = ref<Page>({ const page = ref<Page>({
index: 0, index: 0,
@ -54,11 +47,13 @@ const pageResult = reactive<PageResult<Repository>>({
total: 0, total: 0,
list: [] list: []
}); });
const isLoading = ref(false);
onMounted(async () => { onMounted(async () => {
isLoading.value = true;
const result = await RepositoryAPI.page(page.value); const result = await RepositoryAPI.page(page.value);
pageResult.total = result.total; pageResult.total = result.total;
pageResult.list = result.list; pageResult.list = result.list;
isLoading.value = false;
}); });
</script> </script>
@ -71,25 +66,22 @@ onMounted(async () => {
min-height: 520px; min-height: 520px;
.item { .item {
display: flex;
.icon { padding: 16px;
padding: .5rem;
}
.content { .content {
padding-left: .5rem; flex: 1;
display: flex;
margin-left: 16px;
flex-direction: column;
.header { .header {
display: flex; display: flex;
justify-content: space-between;
.name { .name {
flex: 1; margin: 0;
display: block; font-size: 16px;
.link {
font-size: 16px;
font-weight: bold;
}
} }
} }
} }

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="repository-log"> <div class="repository-log">
<push-log-timeline :items="list"/> <push-log-timeline :items="list" />
<div class="bottom"> <div class="bottom">
<loading :showOn="isLoading"/> <loading :showOn="isLoading" />
<empty-tips :showOn="isFinished && !isLoading"/> <empty-tips :showOn="isFinished && !isLoading" />
<t-button v-show="!isFinished && !isLoading" @click="doFetchEvent">加载更多</t-button> <t-button v-show="!isFinished && !isLoading" @click="doFetchEvent">加载更多</t-button>
</div> </div>
</div> </div>

View File

@ -1,30 +1,27 @@
<template> <template>
<div v-if="repositoryStore.value" class="commit-log"> <div v-if="repositoryStore.value" class="commit-log">
<t-layout> <div class="header">
<t-header class="header"> <t-select class="branch" v-model="branch" label="分支:" placeholder="全部" clearable>
<div> <t-option
<t-select class="branch" v-model="branch" label="分支:" placeholder="全部" clearable> v-for="item in repositoryStore.value.branchList"
<t-option :key="item.name"
v-for="item in repositoryStore.value.branchList" :value="item.name"
:key="item.name" :label="item.name"
:value="item.name" />
:label="item.name" </t-select>
/> </div>
</t-select> <div class="content">
</div> <loading :showOn="isLoading" margin="0 0 1rem 0" />
</t-header> <push-log-timeline :items="commits" :show-repo="false" :show-branch="!branch" />
<t-content class="content"> <div class="more">
<push-log-timeline :items="commits" :show-repo="false" :show-branch="!branch" /> <empty-tips :showOn="isFinished" />
<div class="more"> <t-button v-if="!isLoading && !isFinished" @click="doFetchEvent">加载更多</t-button>
<empty-tips :showOn="isFinished" /> </div>
<t-button v-if="!isFinished" @click="doFetchEvent">加载更多</t-button> </div>
</div>
</t-content>
</t-layout>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { EmptyTips, Page } from "timi-web"; import { EmptyTips, Loading, Page } from "timi-web";
import RepositoryAPI from "@/api/RepositoryAPI"; import RepositoryAPI from "@/api/RepositoryAPI";
import { useRepositoryStore } from "@/store/repository.ts"; import { useRepositoryStore } from "@/store/repository.ts";
import { ActionLogView } from "@/types/Common.ts"; import { ActionLogView } from "@/types/Common.ts";
@ -32,33 +29,32 @@ import PushLogTimeline from "@/components/PushLogTimeline.vue";
const repositoryStore = useRepositoryStore(); const repositoryStore = useRepositoryStore();
const branch = ref();
const page = ref<Page>({ const page = ref<Page>({
index: 0, index: 0,
size: 12 size: 12
}); });
const branch = ref();
const commits = reactive<ActionLogView[]>([]); const commits = reactive<ActionLogView[]>([]);
const isLoading = ref(false);
const isFinished = ref(false); const isFinished = ref(false);
const doFetchEvent = async () => { const doFetchEvent = async () => {
if (repositoryStore.value) { isLoading.value = true;
const result = await RepositoryAPI.pagePush(repositoryStore.value.name, branch.value ?? "all", page.value); const result = await RepositoryAPI.pagePush(repositoryStore.value!.name, branch.value ?? "all", page.value);
const list = result.list; const list = result.list;
commits.push(...list); commits.push(...list);
page.value.index++; page.value.index++;
if (result.list.length < page.value.size) { if (result.list.length < page.value.size) {
isFinished.value = true; isFinished.value = true;
}
} }
isLoading.value = false;
}; };
watch(branch, () => { watch(branch, () => {
if (repositoryStore.value) { page.value.index = 0;
page.value.index = 0; commits.length = 0;
commits.length = 0; isFinished.value = false;
isFinished.value = false; doFetchEvent();
doFetchEvent();
}
}); });
onMounted(doFetchEvent); onMounted(doFetchEvent);
</script> </script>
@ -66,7 +62,8 @@ onMounted(doFetchEvent);
.commit-log { .commit-log {
.header { .header {
top: calc(50px + 49px); top: 96px;
height: 50px;
padding: 0 20px; padding: 0 20px;
display: flex; display: flex;
z-index: 3; z-index: 3;
@ -76,10 +73,15 @@ onMounted(doFetchEvent);
border-bottom: var(--tui-border); border-bottom: var(--tui-border);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
.branch {
width: 12rem;
}
} }
.content { .content {
padding: 1rem 2rem 1rem 1rem; padding: 1rem 2rem 1rem 1rem;
min-height: 520px;
.more { .more {
display: flex; display: flex;

View File

@ -14,6 +14,7 @@
></t-option> ></t-option>
</t-select> </t-select>
<div class="tree-container"> <div class="tree-container">
<loading :showOn="treeLoading" margin="1rem" />
<t-tree <t-tree
ref="tree" ref="tree"
class="tree" class="tree"
@ -28,6 +29,7 @@
:expand-parent="true" :expand-parent="true"
value-mode="all" value-mode="all"
expand-all expand-all
:empty="() => ''"
:load="treeLoad" :load="treeLoad"
:onActive="onTreeActivated" :onActive="onTreeActivated"
> >
@ -35,6 +37,9 @@
<icon name="FILE" fill="GRAY" v-if="node.data.type === FileType.FILE" /> <icon name="FILE" fill="GRAY" v-if="node.data.type === FileType.FILE" />
<icon name="FOLDER" v-if="node.data.type === FileType.DIRECTORY" /> <icon name="FOLDER" v-if="node.data.type === FileType.DIRECTORY" />
</template> </template>
<template #operations="{ node }">
<loading v-if="node.data.isLoading" :size="14" :tips="false" margin="0 .5rem 0 0" />
</template>
</t-tree> </t-tree>
</div> </div>
</pane> </pane>
@ -69,7 +74,7 @@
</split-pane> </split-pane>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Icon } from "timi-web"; import { Icon, Loading } from "timi-web";
import type { TreeInstanceFunctions, TreeProps } from "tdesign-vue-next"; import type { TreeInstanceFunctions, TreeProps } from "tdesign-vue-next";
import RepositoryAPI from "@/api/RepositoryAPI"; import RepositoryAPI from "@/api/RepositoryAPI";
import { File, FileType } from "@/types/Repository"; import { File, FileType } from "@/types/Repository";
@ -100,6 +105,7 @@ onMounted(async () => {
// ---------- 文件树 ---------- // ---------- 文件树 ----------
const tree = ref<TreeInstanceFunctions>(); const tree = ref<TreeInstanceFunctions>();
const treeLoading = ref(false);
const treeItems = ref<File[]>([]); const treeItems = ref<File[]>([]);
const treeMap = { const treeMap = {
value: "path", value: "path",
@ -110,9 +116,14 @@ watch(branch, async () => {
if (!repositoryStore.value || !branch.value) { if (!repositoryStore.value || !branch.value) {
return; return;
} }
treeLoading.value = true;
treeItems.value = await RepositoryAPI.listFile(repositoryStore.value!.name, branch.value, "/"); treeItems.value = await RepositoryAPI.listFile(repositoryStore.value!.name, branch.value, "/");
treeItems.value.forEach((item) => item.children = item.type === FileType.DIRECTORY); treeItems.value.forEach((item) => {
item.isLoading = false;
item.children = item.type === FileType.DIRECTORY;
});
await nextTick(); await nextTick();
treeLoading.value = false;
// 文件树 // 文件树
if (tree.value) { if (tree.value) {
const items = treeItems.value; const items = treeItems.value;
@ -127,6 +138,7 @@ watch(branch, async () => {
const treeLabel: TreeProps["label"] = (_h, node) => node.data.name; const treeLabel: TreeProps["label"] = (_h, node) => node.data.name;
const treeLoad: TreeProps["load"] = async (node) => { const treeLoad: TreeProps["load"] = async (node) => {
const file = node.data; const file = node.data;
file.isLoading = true;
const result = await RepositoryAPI.listFile( const result = await RepositoryAPI.listFile(
repositoryStore.value!.name, repositoryStore.value!.name,
branch.value, branch.value,
@ -138,7 +150,11 @@ const treeLoad: TreeProps["load"] = async (node) => {
file.path = `${file.path}/${deep.name}`; file.path = `${file.path}/${deep.name}`;
return await treeLoad!(node); return await treeLoad!(node);
} }
result.forEach((item) => item.children = item.type === FileType.DIRECTORY); result.forEach((item) => {
item.isLoading = false;
item.children = item.type === FileType.DIRECTORY;
});
file.isLoading = false;
return result; return result;
}; };
@ -192,7 +208,7 @@ const onTreeActivated = (_value: any, context: { node: any }) => {
flex-direction: column; flex-direction: column;
.branch { .branch {
top: calc(50px + 49px); top: 96px;
z-index: 1; z-index: 1;
position: sticky; position: sticky;
border-bottom: var(--tui-border); border-bottom: var(--tui-border);
@ -246,7 +262,7 @@ const onTreeActivated = (_value: any, context: { node: any }) => {
flex-direction: column; flex-direction: column;
.header { .header {
top: calc(50px + 49px); top: 96px;
z-index: 4; z-index: 4;
display: flex; display: flex;
position: sticky; position: sticky;

View File

@ -1,75 +1,65 @@
<template> <template>
<t-layout class="issue-list" v-if="repositoryStore.value"> <div class="issue-list" v-if="repositoryStore.value">
<t-header class="header"> <header class="header">
<t-row justify="space-between"> <div class="search">
<t-col flex="auto"> <t-input-adornment prepend="搜索">
<t-input-adornment prepend="搜索"> <t-input v-model="page.keyword" placeholder="请输入关键字" />
<t-input v-model="page.keyword" placeholder="请输入关键字" /> </t-input-adornment>
</t-input-adornment> </div>
</t-col> <t-space class="filter">
<t-col class="right"> <t-select v-model="page.type" label="类型" placeholder="全部" autoWidth clearable>
<t-space> <t-option v-for="(value, key) in Type" :key="key" :value="key" :label="value" />
<t-select v-model="page.type" label="类型" placeholder="全部" autoWidth clearable> </t-select>
<t-option v-for="(value, key) in Type" :key="key" :value="key" :label="value" /> <t-select v-model="page.status" label="状态" placeholder="全部" autoWidth clearable>
</t-select> <t-option v-for="(value, key) in Status" :key="key" :value="key" :label="value" />
<t-select v-model="page.status" label="状态" placeholder="全部" autoWidth clearable> </t-select>
<t-option v-for="(value, key) in Status" :key="key" :value="key" :label="value" /> <router-link :to="`/${repositoryStore.value.name}/issues/edit`">
</t-select> <t-button theme="success">新建反馈</t-button>
<router-link :to="`/${repositoryStore.value.name}/issues/edit`"> </router-link>
<t-button theme="success">新建反馈</t-button> </t-space>
</router-link> </header>
</t-space> <t-list class="issues" :split="true">
</t-col> <loading :showOn="isLoading" margin="1rem 0" />
</t-row> <empty-tips v-if="!isLoading && pageResult?.total === 0" />
</t-header> <t-list-item class="issue" v-for="item in pageResult?.list" :key="item.id">
<t-content class="list"> <t-space class="tags" :size="4">
<t-list v-if="pageResult" :split="true"> <div
<t-list-item class="action" v-for="item in pageResult.list" :key="item.id"> v-if="item.type"
<t-layout> class="tag type"
<t-aside width="auto"> :class="colorType[(<any>Type)[item.type]]"
<t-space class="tags" :size="4"> v-text="(<any>Type)[item.type]"
<div ></div>
v-if="item.type" <div
class="tag type" v-if="item.status"
:class="colorType[(<any>Type)[item.type]]" class="tag status"
v-text="(<any>Type)[item.type]" :class="colorStatus[(<any>Status)[item.status]]"
></div> v-text="(<any>Status)[item.status]"
<div ></div>
v-if="item.status" </t-space>
class="tag status" <div class="content">
:class="colorStatus[(<any>Status)[item.status]]" <router-link :to="`/${repositoryStore.value.name}/issues/${item.id}`">
v-text="(<any>Status)[item.status]" <t-link theme="default" size="large" hover="color" :content="item.title" />
></div> </router-link>
</t-space> <p class="time gray">
</t-aside> <span v-if="item.closedAt" v-text="`关闭于 ${Time.toPassedDateTime(item.closedAt)}`"></span>
<t-content class="content"> <span v-else v-text="`创建于 ${Time.toPassedDateTime(item.createdAt)}`"></span>
<router-link :to="`/${repositoryStore.value.name}/issues/${item.id}`"> </p>
<t-link theme="default" size="large" hover="color" :content="item.title" /> </div>
</router-link> </t-list-item>
<p class="time gray"> </t-list>
<span v-if="item.closedAt" v-text="`关闭于 ${Time.toPassedDateTime(item.closedAt)}`"></span> <footer v-if="pageResult" class="footer">
<span v-else v-text="`创建于 ${Time.toPassedDateTime(item.createdAt)}`"></span>
</p>
</t-content>
</t-layout>
</t-list-item>
</t-list>
<empty-tips :showOn="pageResult?.total === 0" />
</t-content>
<t-footer class="footer" height="auto">
<t-pagination <t-pagination
v-if="pageResult"
:total="pageResult.total" :total="pageResult.total"
:pageSize="16" :pageSize="16"
:showPageSize="false" :showPageSize="false"
:onCurrentChange="(current: number) => page.index = current - 1" :onCurrentChange="(current: number) => page.index = current - 1"
/> />
</t-footer> </footer>
</t-layout> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { EmptyTips, PageResult, Time, Toolkit } from "timi-web"; import { EmptyTips, Loading, PageResult, Time, Toolkit } from "timi-web";
import IssueAPI from "@/api/IssueAPI"; import IssueAPI from "@/api/IssueAPI";
import { Issue, Page, Status, Type } from "@/types/Issue"; import { Issue, Page, Status, Type } from "@/types/Issue";
import { useRepositoryStore } from "@/store/repository.ts"; import { useRepositoryStore } from "@/store/repository.ts";
@ -96,10 +86,13 @@ const page = reactive<Page>({
index: 0, index: 0,
size: 12 size: 12
}); });
const isLoading = ref(false);
const pageResult = ref<PageResult<Issue>>(); const pageResult = ref<PageResult<Issue>>();
const fetchList = Toolkit.debounce(async () => {
const fetchList = Toolkit.debounce(async () => pageResult.value = await IssueAPI.page(page)); isLoading.value = true;
pageResult.value = await IssueAPI.page(page);
isLoading.value = false;
});
watch(page, fetchList); watch(page, fetchList);
onMounted(async () => { onMounted(async () => {
if (repositoryStore.value) { if (repositoryStore.value) {
@ -111,12 +104,15 @@ onMounted(async () => {
<style lang="less" scoped> <style lang="less" scoped>
.issue-list { .issue-list {
min-height: 480px; display: flex;
flex-direction: column;
justify-content: space-between;
.header { .header {
top: calc(50px + 49px); top: 96px;
z-index: 3; height: 50px;
padding: 0 20px; padding: 0 20px;
z-index: 3;
display: flex; display: flex;
position: sticky; position: sticky;
background: rgba(231, 234, 239, .8); background: rgba(231, 234, 239, .8);
@ -125,43 +121,49 @@ onMounted(async () => {
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
.t-row { .search {
width: 100%; flex: 1;
}
.right {
margin-left: 1rem;
} }
.filter { .filter {
padding: 0 1rem; margin-left: 1rem;
} }
} }
.list { .issues {
flex: 1;
.tags { .issue {
display: flex;
padding-top: 1px;
.tag { :deep(.t-list-item-main) {
padding: 1px 5px; align-items: start;
}
&.type { .tags {
color: #FFF; height: 100%;
display: flex;
padding-top: 1px;
.tag {
padding: 1px 5px;
&.type {
color: #FFF;
}
} }
} }
}
.content { .content {
padding-left: .5rem; flex: 1;
padding-left: .5rem;
.title { .title {
margin: 0; margin: 0;
} }
.time { .time {
margin: 0; margin: 0;
}
} }
} }
} }
@ -176,10 +178,4 @@ onMounted(async () => {
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
} }
} }
@media screen and (max-width: 992px) {
.issue-list {
min-height: calc(100vh - 100px);
}
}
</style> </style>

View File

@ -1,90 +1,65 @@
<template> <template>
<t-layout v-if="repositoryStore.value" class="merge-list"> <div v-if="repositoryStore.value" class="merge-list">
<t-header class="header"> <header class="header">
<t-row justify="space-between"> <div class="search">
<t-col flex="auto"> <t-input-adornment prepend="搜索">
<t-input-adornment prepend="搜索"> <t-input v-model="page.keyword" placeholder="请输入关键字" />
<t-input v-model="page.keyword" placeholder="请输入关键字" /> </t-input-adornment>
</t-input-adornment> </div>
</t-col> <t-space class="filter">
<t-col class="right"> <t-select v-model="page.type" label="类型" placeholder="全部" autoWidth clearable>
<t-space> <t-option v-for="(value, key) in Type" :key="key" :value="key" :label="value" />
<t-select </t-select>
v-model="page.type" <t-select v-model="page.status" label="状态" placeholder="全部" autoWidth clearable>
label="类型" <t-option v-for="(value, key) in Status" :key="key" :value="key" :label="value" />
placeholder="全部" </t-select>
autoWidth <router-link :to="`/${repositoryStore.value.name}/merges/edit`">
clearable <t-button theme="success">申请合并</t-button>
> </router-link>
<t-option v-for="(value, key) in Type" :key="key" :value="key" :label="value" /> </t-space>
</t-select> </header>
<t-select <t-list class="merges" v-if="pageResult" :split="true">
v-model="page.status" <loading :showOn="isLoading" margin="1rem 0" />
label="状态" <empty-tips v-if="!isLoading && pageResult?.total === 0" />
placeholder="全部" <t-list-item class="merge" v-for="item in pageResult.list" :key="item.id">
autoWidth <t-space class="tags" :size="4">
clearable <div
> v-if="item.type"
<t-option v-for="(value, key) in Status" :key="key" :value="key" :label="value" /> class="tag type"
</t-select> :class="colorType[(<any>Type)[item.type]]"
<router-link :to="`/${repositoryStore.value.name}/merges/edit`"> v-text="(<any>Type)[item.type]"
<t-button theme="success">申请合并</t-button> ></div>
</router-link> <div
</t-space> v-if="item.status"
</t-col> class="tag status"
</t-row> :class="colorStatus[(<any>Status)[item.status]]"
</t-header> v-text="(<any>Status)[item.status]"
<t-content class="list"> ></div>
<t-list v-if="pageResult" :split="true"> </t-space>
<t-list-item class="action" v-for="item in pageResult.list" :key="item.id"> <div class="content">
<t-layout> <router-link :to="`/${repositoryStore.value.name}/merges/${item.id}`">
<t-aside width="auto"> <t-link theme="default" size="large" hover="color" :content="item.title" />
<t-space class="tags" :size="4"> </router-link>
<div <p class="time gray">
v-if="item.type" <span v-if="item.rejectedAt" v-text="`拒绝于 ${Time.toPassedDateTime(item.rejectedAt)}`"></span>
class="tag type" <span v-else v-text="`申请于 ${Time.toPassedDateTime(item.createdAt)}`"></span>
:class="colorType[(<any>Type)[item.type]]" </p>
v-text="(<any>Type)[item.type]" </div>
></div> </t-list-item>
<div </t-list>
v-if="item.status" <footer v-if="pageResult" class="footer">
class="tag status"
:class="colorStatus[(<any>Status)[item.status]]"
v-text="(<any>Status)[item.status]"
></div>
</t-space>
</t-aside>
<t-content class="content">
<router-link :to="`/${repositoryStore.value.name}/merges/${item.id}`">
<t-link theme="default" size="large" hover="color" :content="item.title" />
</router-link>
<p class="time gray">
<span
v-if="item.rejectedAt"
v-text="`拒绝于 ${Time.toPassedDateTime(item.rejectedAt)}`"
></span>
<span v-else v-text="`申请于 ${Time.toPassedDateTime(item.createdAt)}`"></span>
</p>
</t-content>
</t-layout>
</t-list-item>
</t-list>
<empty-tips :showOn="pageResult?.total === 0" />
</t-content>
<t-footer class="footer" height="auto">
<t-pagination <t-pagination
v-if="pageResult"
:total="pageResult.total" :total="pageResult.total"
:pageSize="16" :pageSize="16"
:showPageSize="false" :showPageSize="false"
:onCurrentChange="(current: number) => page.index = current - 1" :onCurrentChange="(current: number) => page.index = current - 1"
/> />
</t-footer> </footer>
</t-layout> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { EmptyTips, PageResult, Time, Toolkit } from "timi-web"; import { EmptyTips, Loading, PageResult, Time, Toolkit } from "timi-web";
import MergeAPI from "@/api/MergeAPI"; import MergeAPI from "@/api/MergeAPI";
import { Merge, Page, Status, Type } from "@/types/Merge"; import { Merge, Page, Status, Type } from "@/types/Merge";
import { useRepositoryStore } from "@/store/repository.ts"; import { useRepositoryStore } from "@/store/repository.ts";
@ -105,11 +80,13 @@ const colorStatus = {
}; };
const repositoryStore = useRepositoryStore(); const repositoryStore = useRepositoryStore();
const page = reactive<Page>({ const page = reactive<Page>({
repositoryId: undefined, repositoryId: undefined,
index: 0, index: 0,
size: 12 size: 12
}); });
const isLoading = ref(false);
const pageResult = ref<PageResult<Merge>>(); const pageResult = ref<PageResult<Merge>>();
const fetchList = Toolkit.debounce(async () => pageResult.value = await MergeAPI.page(page)); const fetchList = Toolkit.debounce(async () => pageResult.value = await MergeAPI.page(page));
@ -124,11 +101,13 @@ onMounted(async () => {
<style lang="less" scoped> <style lang="less" scoped>
.merge-list { .merge-list {
min-height: 480px; display: flex;
flex-direction: column;
justify-content: space-between;
.header { .header {
top: calc(50px + 49px); top: 96px;
z-index: 3; height: 50px;
padding: 0 20px; padding: 0 20px;
display: flex; display: flex;
position: sticky; position: sticky;
@ -138,43 +117,49 @@ onMounted(async () => {
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
.t-row { .search {
width: 100%; flex: 1;
}
.right {
margin-left: 1rem;
} }
.filter { .filter {
padding: 0 1rem; margin-left: 1rem;
} }
} }
.list { .merges {
flex: 1;
.tags { .merge {
display: flex;
padding-top: 1px;
.tag { :deep(.t-list-item-main) {
padding: 1px 5px; align-items: start;
}
&.type { .tags {
color: #FFF; height: 100%;
display: flex;
padding-top: 1px;
.tag {
padding: 1px 5px;
&.type {
color: #FFF;
}
} }
} }
}
.content { .content {
padding-left: .5rem; flex: 1;
padding-left: .5rem;
.title { .title {
margin: 0; margin: 0;
} }
.time { .time {
margin: 0; margin: 0;
}
} }
} }
} }
@ -185,15 +170,8 @@ onMounted(async () => {
position: sticky; position: sticky;
background: rgba(255, 255, 255, .8); background: rgba(255, 255, 255, .8);
border-top: var(--tui-border); border-top: var(--tui-border);
border-top: var(--tui-border);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
} }
} }
@media screen and (max-width: 992px) {
.merge-list {
min-height: calc(100vh - 100px);
}
}
</style> </style>

View File

@ -37,14 +37,15 @@
</div> </div>
</div> </div>
</t-timeline-item> </t-timeline-item>
<loading :showOn="isLoading" />
<empty-tips v-if="!isLoading && Toolkit.isEmpty(pageResult?.list)" />
</t-timeline> </t-timeline>
<empty-tips :showOn="Toolkit.isEmpty(pageResult?.list)" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import ReleaseAPI from "@/api/ReleaseAPI"; import ReleaseAPI from "@/api/ReleaseAPI";
import { Page, Release } from "@/types/Release"; import { Page, Release } from "@/types/Release";
import { CommonAPI, EmptyTips, Icon, IOSize, MarkdownView, PageResult, Time, Toolkit } from "timi-web"; import { CommonAPI, EmptyTips, Icon, IOSize, Loading, MarkdownView, PageResult, Time, Toolkit } from "timi-web";
import { useRepositoryStore } from "@/store/repository.ts"; import { useRepositoryStore } from "@/store/repository.ts";
const repositoryStore = useRepositoryStore(); const repositoryStore = useRepositoryStore();
@ -54,13 +55,14 @@ const page = reactive<Page>({
index: 0, index: 0,
size: 12 size: 12
}); });
const isLoading = ref(false);
const pageResult = ref<PageResult<Release>>(); const pageResult = ref<PageResult<Release>>();
onMounted(async () => { onMounted(async () => {
if (repositoryStore.value) { isLoading.value = true;
page.repositoryId = repositoryStore.value.id; page.repositoryId = repositoryStore.value!.id;
pageResult.value = await ReleaseAPI.page(page); pageResult.value = await ReleaseAPI.page(page);
} isLoading.value = false;
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -121,10 +123,4 @@ onMounted(async () => {
} }
} }
} }
@media screen and (max-width: 992px) {
.release-list {
min-height: calc(100vh - 98px);
}
}
</style> </style>