搜索结果列表页面开发

This commit is contained in:
付民康
2025-03-12 18:41:20 +08:00
commit 7cebe8fc00
739 changed files with 88149 additions and 0 deletions

View File

@@ -0,0 +1,420 @@
<template>
<div class="org-title text-G900 text-2xl mt-24 mb-24 font-[600]">{{ createOrgItem.title }}</div>
<Card class="org-card pl-32 pr-32">
<div class="org-contentdiv overflow-hidden">
<d-form class="org" ref="formRef" layout="vertical" :data="orgData" :pop-position="['right']" :rules="rules">
<d-form-item field="name" :rules="rules.name" :show-feedback="false" :label="createOrgItem.orgName.label">
<d-input v-model="orgData.name" style="width: 668px;" maxlength="50"
:placeholder="createOrgItem.orgName.placeholder" @blur="handleNameBlur" />
</d-form-item>
<d-form-item class="org-url" field="path" :rules="rules.path" :label="createOrgItem.orgUrl.label"
:help-tips="createOrgItem.orgUrl.desc" :extra-info="(true) ? '' : createOrgItem.orgUrl.occupied">
<div class="org-path w-[fit-content]">
<d-input class="org-prefix" v-model="createOrgItem.orgUrl.prefix" :validate-event="false" disabled />
<div class="org-path-line">/</div>
<d-input v-model="orgData.path" :placeholder="createOrgItem.orgUrl.placeholder" maxlength="50" />
</div>
</d-form-item>
<d-form-item field="orgData.avatar" :label="createOrgItem.orgLogo.label" class="org-logo">
<div class="img-container" v-if="!showUpload">
<img class="img-preview" :src="orgData.avatar" />
<div class="delete-img" @click="handleDel">
<d-icon name="delete" size="14px"></d-icon>
</div>
</div>
<d-upload class="upload-demo-new" accept=".png,.jpg,.gif,.jpeg" :before-upload="beforeUpload" v-if="showUpload"
@file-select="fileSelect">
<div class="upload-trigger">
<d-icon name="add" size="24px"></d-icon>
</div>
</d-upload>
</d-form-item>
<d-form-item field="visibility" :label="createOrgItem.visibility.label">
<d-radio-group direction="row" v-model="orgData.visibility">
<d-radio v-for="(item, index) in visibility" :key="item.value" :value="item.value"
:class="index != 0 ? 'org-radio' : ''">
<div class="org-radiondiv">
<Icon :name="item.icon" class="mr-1"></Icon>
{{ item.name }}
</div>
</d-radio>
</d-radio-group>
</d-form-item>
</d-form>
</div>
<div class="org-radiondiv pt-[12px] w-[668px] text-right">
<d-button @click="cancel">取消</d-button>
<d-button variant="solid" class="org-cancel" color="primary" :loading="btnLoading" :disabled="!isFormValid"
@click="handleSubmit">
创建组织
</d-button>
</div>
</Card>
</template>
<script setup lang="ts">
import { reactive, ref, watch, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import debounce from 'lodash/debounce';
import { reqCatch } from '@/utils/catch';
import { createOrg, inquireOrgPathIsRepeat } from '@/api/org';
import * as types from '@/api/org/types';
import { uploadFile } from '@/api/user';
import { Message } from 'vue-devui/message';
import { orgNameRegExp, orgPathRegExp } from '@/utils/regex';
import { orgInfoStore } from '@/stores/Org/index';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { getUserCreateOrg } = getOrgInfo();
getUserCreateOrg();
const orgStore = orgInfoStore();
const router = useRouter();
watch(() => orgStore.isCreateOrg, () => {
orgCreateFun();
});
const orgCreateFun = () => {
if (!orgStore.isCreateOrg) {
Message({
type: 'warning',
message: '你最多可以创建或管理 5 个组织,无法创建新的组织。'
});
router.replace({
name: 'settingOrganization'
});
}
};
orgCreateFun();
const showUpload = ref(true);
const btnLoading = ref(false);
const beforeUpload = (file: any) => {
// if (file.length > 0 && file[0].size > 200 * 1024) {
// return Message({
// type: 'warning',
// message: '图片大小不超过200kb!'
// });
// }
// const reader = new FileReader();
// reader.onload = async() => {
// showUpload.value = false;
// await nextTick();
// document.getElementsByClassName('img-preview')[0].src = reader.result;
// };
// reader.readAsDataURL(file[0].file);
return false;
};
const handleDel = () => {
showUpload.value = true;
orgData.avatar = '';
};
const visibility = ref([
{
value: 'public',
name: '公开',
icon: 'gt-public-c'
},
{
value: 'private',
name: '私密',
icon: 'gt-lock-c'
}
]);
interface orgDataObj {
name: string,
path: string,
avatar: string,
visibility: string,
}
const orgData = reactive<orgDataObj>({
name: '',
path: '',
avatar: '',
visibility: 'public'
});
const createOrgItem = {
title: '新建组织',
orgName: {
label: '组织名称',
placeholder: '请输入组织名称',
ruleDesc: {
notNull: '组织名称不能为空'
// regex: '组织名称由字母、数字和连字符(-)组成;组织名称必须以字母或数字开头;连续的连字符(-)是不允许的;组织名称最多可以包含 50 个字符。'
}
},
orgUrl: {
label: '组织URL',
prefix: `${(import.meta as any).env.VITE_HOST}/`,
placeholder: '请输入',
occupied: 'namespace已被使用',
ruleDesc: {
notNull: 'url不能为空',
regex: ''
}
},
orgLogo: {
label: '组织LOGO',
desc: '图片宽度*高度至少为150*150像素大小不超过200KB。',
placeholder: '将图片拖到此处,或',
button: '点击上传',
delete: '删除'
},
visibility: {
label: '组织可见性',
default: '公开',
internal: '私密'
},
createOrg: '创建组织',
cancel: '取消'
};
// 验证path是否重复
const isPathValid = ref<boolean>(false);
const lastPath = ref<string>('');
const formRef = ref(null);
const inquireOrgPath = async() => {
if (orgData.path) {
if (orgData.path !== lastPath.value) {
const params: types.commonGroupReqType = {
path_name: orgData.path
};
const res = await reqCatch(inquireOrgPathIsRepeat, params);
isPathValid.value = (!res.data) || (res.data.data.devpress_exist !== false) || (res.data.data.type !== null);
lastPath.value = orgData.path;
}
} else {
isPathValid.value = false;
}
// validateForm();
};
const validateForm = () => new Promise((resolve, reject) => {
formRef.value.validate((isValid: boolean, invalidFields: any) => {
isFormValid.value = isValid;
isValid ? resolve(true) : reject(invalidFields);
});
});
// watch(() => orgData.path, debounce(validateForm, 500));
const checkOrgName = (rule: object, value: string, callback: Function) => {
isFormValid.value = false;
if (!value) {
return callback(new Error('组织名称不能为空!'));
}
if (!orgNameRegExp.test(value)) {
return callback(
new Error(
'组织名称仅包含中文、字母、数字、下划线和连字符(-不支持连字符或下划线开头和结尾不支持连续的连字符或下划线最多50个字符。'
)
);
}
isFormValid.value = true;
return callback();
};
const checkOrgPath = async(rule: object, value: string, callback: Function) => {
isFormValid.value = false;
if (!value) {
lastPath.value = '';
return callback(new Error('组织路径不能为空!'));
}
if (!orgPathRegExp.test(value)) {
lastPath.value = '';
return callback(
new Error(
'路径须以字母开头,以字母或数字结尾,仅包含字母(大小写不敏感)、数字、连字符(-、下划线_长度在3到50个字符之间。'
)
);
}
if (~['v1', 'backend', 'organization', 'public'].indexOf(value)) {
lastPath.value = '';
return callback(new Error('路径已被使用,请重新输入'));
}
// 验证重复
await inquireOrgPath();
if (isPathValid.value) {
// lastPath.value = '';
return callback(new Error('路径已被使用,请重新输入'));
}
isFormValid.value = true;
return callback();
};
const rules = {
name: [{required: true, message: '组织名称不能为空!'},{ validator: checkOrgName }],
path: [{required: true, message: '组织路径不能为空!'},{ validator: checkOrgPath }],
logo: ''
};
// 上传图片
const imgLoading = ref<boolean>(false);
const fileSelect = async(fileInfo: any) => {
if (fileInfo.length > 0 && fileInfo[0].size > 200 * 1024) {
return Message({
type: 'warning',
message: '图片大小不超过200kb!'
});
}
imgLoading.value = true;
const resImg: any = await uploadFile(fileInfo, false, fileInfo[0].type);
orgData.avatar = resImg + '?time' + new Date().getTime();
imgLoading.value = false;
showUpload.value = false;
};
const isFormValid = ref(false);
const handleSubmit = async() => {
try {
await validateForm();
await create();
} catch (err) {
// ignore
}
};
const create = async() => {
btnLoading.value = true;
const params = { // 暂定只需要传递name,path,visibility
name: orgData.name,
path: orgData.path,
visibility: orgData.visibility,
avatar: orgData.avatar
};
const res = await reqCatch(createOrg, params);
btnLoading.value = false;
if (!res.error) {
router.push({
name: 'homepage',
params: {
namespace: orgData.path,
repoName: orgData.name
}
});
}
};
const cancel = () => {
router.go(-1);
};
// 输入名称补全path
const handleNameBlur = () => {
if (orgPathRegExp.test(orgData.name)) {
orgData.path = orgData.name;
}
};
</script>
<style lang="scss" scoped>
.org {
:deep(.devui-form__label--required:before) {
margin-left: 0px;
}
&-prefix {
:deep(.devui-input--error) {
background: #FBFBFB;
border-color: #E6E7E8;
color: #7E7E80;
}
}
:deep(.devui-radio) {
display: flex;
align-items: center;
}
&-path {
display: flex;
:deep(.devui-input) {
width: 326px;
}
&-line {
padding: 5px 6px;
font-size: 18px;
}
}
:deep(.devui-form__control .devui-form__control-info .error-message) {
white-space: nowrap;
margin: 8px auto 20px auto;
}
}
.org-card {
padding: 20px;
}
.org-cancel {
// width: 96px;
margin-left: 8px;
}
.org-contentdiv {
width: 888px;
}
.org-imgdiv {
display: flex;
align-items: flex-end;
margin-bottom: 8px;
}
.org-radiondiv {
.org-radioicon {
margin-right: 8px;
}
}
.org-imgbg {
width: 120px;
height: 120px;
}
.org-logo-desc {
font-weight: 400;
line-height: 20px;
}
:deep(.devui-icon--no-slots i) {
display: inline;
}
.upload-demo-new .upload-trigger {
background-color: #fff;
border: 1px dashed #d9d9d9;
border-radius: 6px;
box-sizing: border-box;
width: 180px;
height: 180px;
text-align: center;
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
}
.upload-demo .upload-trigger .link {
color: #5e7ce0;
}
.img-container {
position: relative;
}
.img-preview {
width: 180px;
height: 180px;
border-radius: 6px;
}
.delete-img {
width: 24px;
height: 24px;
background-color: #f7f7f9;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: -12px;
left: 168px;
}
</style>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import DiscussionCreate from '@/components/Discussion/Module/Create/index.vue';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { namespace } = getOrgInfo();
</script>
<template>
<DiscussionCreate :source-type="1" :org-namespace="namespace"></DiscussionCreate>
</template>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import DiscussionDetail from '@/components/Discussion/Module/Detail/index.vue';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { namespace } = getOrgInfo();
</script>
<template>
<DiscussionDetail :source-type="1" :org-namespace="namespace"></DiscussionDetail>
</template>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import DiscussionSectionForm from '@/components/Discussion/Module/SectionCreate/index.vue';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { namespace } = getOrgInfo();
</script>
<template>
<DiscussionSectionForm :source-type="1" :org-namespace="namespace"></DiscussionSectionForm>
</template>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import DiscussionSelectType from '@/components/Discussion/Module/Select/index.vue';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { namespace } = getOrgInfo();
</script>
<template>
<DiscussionSelectType :source-type="1" :org-namespace="namespace"></DiscussionSelectType>
</template>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import DiscussionTypeForm from '@/components/Discussion/Module/TypeCreate/index.vue';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { namespace } = getOrgInfo();
</script>
<template>
<DiscussionTypeForm :source-type="1" :org-namespace="namespace"></DiscussionTypeForm>
</template>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import DiscussionTypeManage from '@/components/Discussion/Module/TypeManage/index.vue';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { namespace } = getOrgInfo();
</script>
<template>
<DiscussionTypeManage :source-type="1" :org-namespace="namespace"></DiscussionTypeManage>
</template>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import DiscussionList from '@/components/Discussion/Module/List/orgDiscussion.vue';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { namespace } = getOrgInfo();
</script>
<template>
<DiscussionList :source-type="1" :org-namespace="namespace"></DiscussionList>
</template>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,9 @@
<template>
<IssueFilterList type="organization" :organizationId="namespace"/>
</template>
<script lang="ts" setup>
import IssueFilterList from '@/views/Org/Issue/issueList.vue';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { namespace } = getOrgInfo();
</script>

View File

@@ -0,0 +1,521 @@
<template>
<div class="page-filter-list">
<teleport v-if="props.formElem" :to="props.formElem">
<div class="flex">
<SearchBar v-if="type !== 'milestone'" :default-keys="keys" :labelTotal="labelTotal"
:milestoneTotal="milestoneTotal" :hiddenNav="type !== 'project'" :hiddenCreate="type !== 'project'"
:hiddenSelect="false" :keyTags="locationSearch" @tag-delete="removeLocationKey($event)"
@inputChange="(value) => (keys.search = value)" @select-change="(value) => (keys.scope = value)" :type-options="[
{ name: '全部', value: 'all' },
{ name: '我创建的', value: 'created_by_me' },
{ name: '分配给我的', value: 'assigned_to_me' },
]" class="mr-2">
<template #create>
<d-button :icon="issueLoading ? '' : 'add'" :loading="issueLoading" class="search-bar-create" @click="create" variant="solid"
v-if="type === 'project' && !isArchived && issueLink">新建 Issue</d-button>
</template>
</SearchBar>
<div class="page-filter-list-header">
<StateSwitchTab :default-state="keys.state" :tab-list="tabList"
@active-tab-change="(value) => changeTab(value)">
</StateSwitchTab>
<div class="page-filter-dropdowngroup">
<div class="flex gap-4 items-center">
<!-- 负责人 -->
<d-dropdown v-if="type === 'project' && isVisitorOperate" :position="['bottom-start', 'top-start']"
align="start" :destroy-on-hide="false" close-scope="blank"
@toggle="(blo: boolean) => !blo && RefAssignee.clear()" overlay-class="g-z-index-9999">
<d-button variant="text">
<div class="filter-dropdown-item">
负责人<d-icon name="icon-select-arrow" />
</div>
</d-button>
<template #menu>
<IssueFilterAssignee title="按负责人过滤" placeholder="搜索用户" :repoId="repoId"
:selected-list="[formData.assignee]" hidden-tab optionClickScope
@on-option-click="onAssigneeOptionClick" ref="RefAssignee">
</IssueFilterAssignee>
</template>
</d-dropdown>
<span class="g-mid-line" v-if="type === 'project' && isVisitorOperate"></span>
<!-- label -->
<d-dropdown v-if="type === 'project'" :position="['bottom-start', 'top-start']" align="start"
:destroy-on-hide="false" close-scope="blank" @toggle="(blo: boolean) => !blo && RefFilterLabel.clear()"
overlay-class="g-z-index-9999">
<d-button variant="text">
<div class="filter-dropdown-item">
Label
<d-icon name="icon-select-arrow" />
</div>
</d-button>
<template #menu>
<IssueFilterLabel title=" Label 过滤" :target_project_id="repoId" :selected-list="formData.labels"
placeholder="搜索label" hidden-tab @on-option-click="onLabelOptionClick" ref="RefFilterLabel">
</IssueFilterLabel>
</template>
</d-dropdown>
<span class="g-mid-line" v-if="type === 'project'"></span>
<!-- 里程碑 -->
<d-dropdown v-if="type === 'project'" :position="['top', 'bottom']" :destroy-on-hide="false"
close-scope="blank" @toggle="(blo: boolean) => !blo && RefMilestone.clear()" overlay-class="g-z-index-9999">
<d-button variant="text">
<div class="filter-dropdown-item">
里程碑
<d-icon name="icon-select-arrow" />
</div>
</d-button>
<template #menu>
<IssueFilterMilestone title="按里程碑过滤" :repoId="repoId" :selected-list="[formData.milestone]"
placeholder="搜索里程碑" hidden-tab @on-option-click="onMilestoneOptionClick" optionClickScope
ref="RefMilestone">
</IssueFilterMilestone>
</template>
</d-dropdown>
<span class="g-mid-line" v-if="type === 'project'"></span>
<!-- 排序 -->
<d-dropdown :position="['top-end', 'bottom-end']" align="start" :destroy-on-hide="false" close-scope="all"
overlay-class="g-z-index-9999">
<d-button variant="text">
<div class="filter-dropdown-item">
排序
<d-icon name="icon-select-arrow" class="right-arrow" />
</div>
</d-button>
<template #menu>
<CustomOption v-for="item in sortOption" :key="item.value" :selected="sortAndOrder == item.value"
:value="item.value" @click="setSort(item)">{{ item.label }}</CustomOption>
</template>
</d-dropdown>
</div>
</div>
</div>
</div>
</teleport>
<data-panel :empty="!issueList[0] && !pager.loading" class="overflow-hidden" :card="false">
<d-table :show-loading="pager.loading" :data="issueList" :show-header="false" :row-hovered-highlight="false"
size="lg" empty="暂无数据" class="table-box">
<!-- state -->
<d-column :width="36" cell-class="state-icon">
<template #default="{ row }">
<Icon v-if="row?.state && issueStateOption[row?.state]" :name="issueStateOption[row?.state].icon"
:color="issueStateOption[row?.state].color"></Icon>
</template>
</d-column>
<!-- info -->
<d-column>
<template #default="{ row }">
<IssueBlurb :projectPath="row.project?.path_with_namespace" :id="row.iid" :title="row.title"
:stateIcon="issueStateOption[row?.state].icon" :stateColor="issueStateOption[row?.state].color"
:labels="row.labels" :idUrl="$router.resolve({
name: 'repoIssueDetail',
params: {
namespace: getNamespacesFun(row.project?.path_with_namespace),//row.project?.path_with_namespace?.split('/')[0],
repoName: getRepoNameFun(row.project?.path_with_namespace),//row.project?.path_with_namespace?.split('/')[1],
serialNumber: row.iid,
},
})?.href.replace(/%2F/g, '/')
" :author="row.author" :createdAt="row.created_at" :updatedAt="row.updated_at"
:milestoneTitle="row.milestone?.title" :userNotesCount="row.user_notes_count"
:mergeRequestsCount="row.merge_requests_count" />
</template>
</d-column>
<template #empty>
<d-skeleton :loading="pager.loading">
<template #placeholder>
<div v-for="i in 1" :key="i" class="bg-white py-4 px-4 flex items-center gap-4 my-[-40px]">
<SkeletonItem variant="circle"
style="width: 16px; height: 16px; align-self: flex-start;"></SkeletonItem>
<div class="flex-grow">
<SkeletonItem style="width: 80%;"></SkeletonItem>
<div class="flex gap-4 mt-2">
<SkeletonItem v-for="i in 6" :key="i" style="width: 50px; height: 16px;"></SkeletonItem>
</div>
</div>
</div>
</template>
</d-skeleton>
</template>
</d-table>
</data-panel>
<teleport v-if="props.pageElem" :to="props.pageElem">
<d-pagination auto-hide class="px-[20px] py-[20px] flex justify-center"
:page-size-options="pageOptions.pageSizeOptions" :show-page-selector="false" :total="pager.total"
v-model:pageSize="pager.pageSize" v-model:pageIndex="pager.page" :can-view-total="true"
:can-change-page-size="true" :max-items="pageOptions.maxItems" @page-size-change="pageSizeChange"
:max-page="500" />
</teleport>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'IssueFilterList' });
import { ref, onMounted, onBeforeMount } from 'vue';
import SearchBar from '@/views/Repo/components/SearchBar/index.vue';
import CustomOption from '@/components/FilterDropDown/CustomOption.vue';
import IssueFilterMilestone from '@/views/Repo/components/FilterDropDown/IssueFilterMilestone.vue';
import IssueFilterLabel from '@/views/Repo/components/FilterDropDown/IssueFilterLabel.vue';
import IssueFilterAssignee from '@/views/Repo/components/FilterDropDown/IssueFilterAssignee.vue';
import StateSwitchTab from '@/components/StateSwitchTab/index.vue';
import IssueBlurb from '@/views/Repo/components/TableItemBlurb/index.vue';
import { fetchIssueList, myIssueList, groupIssueList } from '@/api/issue';
import { getRepoMilestones } from '@/api/repo';
import { getProLabels } from '@/api/labels';
import { reqCatch } from '@/utils/catch';
import type { IMilestone } from '@/api/milestone/types';
import debounce from 'lodash/debounce';
import { removeEmptyValue, formatSelectedData } from '@/utils';
import type { IAuthor, ILabel } from '@/api/issue/types';
import { issueStateOption } from '@/constant/issue';
import { repoInfoStore } from '@/stores/Repo/index';
import { useSearch } from '@/views/Repo/hooks/useSearch';
import { useOrgId } from '@/utils/hooks/useOrgId';
import { useGlobalInfoStore } from '@/stores/Global';
import { useAccountStore } from '@/stores/user';
const userStore = useAccountStore();
import { emitEvent } from '@/utils/eventBus';
import { useRouter } from 'vue-router';
import isEqual from 'lodash/isEqual';
import { storeToRefs } from 'pinia';
import { useIssueTemplate } from '@/utils/hooks/useIssueTemplate';
import { useRepoId } from '@/utils/hooks/useRepoId';
const { issueLink, getIssueInfo, issueLoading } = useIssueTemplate();
const { repoId } = useRepoId();
const { updateMenuNum } = useGlobalInfoStore();
// 获取组织namespace
const { orgId } = useOrgId('/');
const { isVisitorOperate, isArchived } = storeToRefs(repoInfoStore());
const props = withDefaults(
defineProps<{
repoId?: string;
organizationId?: string;
formElem: null | HTMLElement;
pageElem: null | HTMLElement;
type: 'organization' | 'project' | 'milestone' | 'personal'; // issue 关联主体
searchParams?: Object;
}>(),
{ formElem: null, pageElem: null }
);
const router = useRouter();
const RefAssignee = ref();
const RefFilterLabel = ref();
const RefMilestone = ref();
const labelTotal = ref(0);
const milestoneTotal = ref(0);
// selectable
const tabList = ref([
{ count: 0, id: 'opened', title: '已开启' },
{ count: 0, id: 'closed', title: '已关闭' },
{ count: 0, id: 'all', title: '全部' }
]);
const {
keys,
sortAndOrder,
formData,
locationSearch,
pager,
pageOptions,
sortOption,
getKeys,
removeLocationKey,
addLocationKey,
clearLocationSearch
} = useSearch({
fetch: () => fetchIssueListData(),
sortOptionType: 'issue',
storageKey: 'issue-' + props?.type,
clientType: 'pc'
});
const issueList = ref([]);
onMounted(() => {
if (props.type !== 'milestone') {
fetchIssueListData();
}
if (props.type === 'project') getNavCount();
});
onBeforeMount(() => {
clearLocationSearch();
});
const create = async () => {
if (userStore.isLogin) {
if (issueLoading.value) return;
await getIssueInfo(repoId.value || '')
router.push({ name: issueLink.value });
return;
}
emitEvent('login');
};
const changeTab = (value: any) => {
keys.state = value;
};
/**
* 项目,组织,或我的 issue 筛选
*/
const fetchIssueListData = debounce(async() => {
pager.loading = true;
let fetchApi;
if (props.type === 'personal') {
// 我的issue
fetchApi = myIssueList;
} else if (props.type === 'project') {
// 项目issue
fetchApi = fetchIssueList;
} else if (props.type === 'milestone') {
// 里程碑issue
fetchApi = fetchIssueList;
} else if (props.type === 'organization') {
// 组织issue
fetchApi = groupIssueList;
} else {
throw new Error('缺少issue 归属');
}
// 去除空参数
const params = removeEmptyValue({
...getKeys(),
...props.searchParams,
project_id: props?.repoId,
group_id: props.organizationId,
page: pager.page,
per_page: pager.pageSize
});
const res = await reqCatch(fetchApi, params);
pager.loading = false;
if (!res.error) {
let { issues, content, total } = res.data.data;
issues = issues || content || [];
// 获取tab 数量
if (props.type === 'organization') {
issues = content?.issues || [];
// tab 栏 数量
for (let i = 0; i < tabList.value.length; i++) {
const element = tabList.value[i];
element.count = content[element.id];
if (keys.state === element.id) {
pager.total = element.count;
}
}
} else if (props.type === 'personal') {
// 当前状态数量
pager.total = total;
tabList.value.find((item) => item.id === params.state).count = total;
// 其他状态数量
const tabs = tabList.value
.filter((item) => item.id !== keys.state)
.map((e) => e.id);
Promise.all(
tabs.map((value) =>
myIssueList({ ...params, state: value, page: 1, per_page: 1 })
)
).then((res) => {
tabList.value.forEach((item) => {
const index = tabs.findIndex((t) => t === item.id);
if (index > -1) {
item.count = res[index].data.total;
}
});
});
} else {
for (let i = 0; i < tabList.value.length; i++) {
const element = tabList.value[i];
element.count = res.data.data[element.id];
if (keys.state === element.id) {
pager.total = element.count;
}
}
// 当获取所有已开启issue时更新toolbar数量
const openFilterParams = {
state: 'opened',
sort: 'desc',
scope: 'all',
page: 1,
per_page: 10
};
delete params.project_id;
if (isEqual(openFilterParams, params)) {
updateMenuNum('repoIssues', pager.total);
}
}
issueList.value = issues;
}
pager.loading = false;
}, 100);
// 里程碑
const onMilestoneOptionClick = async(data: IMilestone) => {
if (formData.milestone?.title === data?.title || !data) {
formData.milestone = null;
} else {
formData.milestone = data;
}
};
// 筛选label
const onLabelOptionClick = async(data: ILabel) => {
const selectList = formatSelectedData(
data,
formData?.labels?.slice(0) || [],
(a, b) => a.name === b.name
);
formData.labels = selectList;
};
// 负责人
const onAssigneeOptionClick = (data: IAuthor) => {
if (formData?.assignee?.id === data?.id || !data) {
formData.assignee = null;
keys.assignee_id = '';
} else {
formData.assignee = data;
keys.assignee_id = data.id;
}
};
// 排序
const setSort = (obj: any) => {
if (sortAndOrder.value === obj.value) {
sortAndOrder.value = '';
keys.sort = '';
keys.order_by = '';
document.body.click();
return;
}
keys.sort = obj.sort;
keys.order_by = obj.order_by;
sortAndOrder.value = obj.value;
document.body.click();
};
const pageSizeChange = (size: number) => {
document.body.click();
};
/* 获取labels 和 里程碑数据 */
const getNavCount = async() => {
const resLabel = await getProLabels({
project_id: props.repoId,
page: 1,
per_page: 1
});
if (!resLabel.error) labelTotal.value = resLabel?.data?.data?.total;
const resMilestone = await reqCatch(getRepoMilestones, {
repoId: props.repoId,
page: 1,
per_page: 1
});
if (!resMilestone.error) {
milestoneTotal.value = resMilestone?.data?.data?.total;
}
};
// 获取namespace
const getNamespacesFun = (path: string) => {
const lastSlashIndex = path.lastIndexOf('/');
let namespace = '';
if (lastSlashIndex !== -1) {
namespace = path.substring(0, lastSlashIndex);
}
return namespace;
};
// 获取项目名称
const getRepoNameFun = (path: string) => {
const pathArray = path.split('/');
return pathArray[pathArray.length - 1];
};
</script>
<style lang="scss" scoped>
.page-filter-list {
width: 100%;
gap: 12px 0;
@apply flex flex-col;
&-header {
@apply flex justify-between items-center gap-2;
}
}
.isu-filt-list-header-box {
display: flex;
gap: 12px 0px;
&-right {
color: var(--devui-text, #252b3a);
border-color: var(--devui-line, #d7d8da);
line-height: var(--devui-line-height-base, 1.5);
border-radius: var(--border-radius);
border-width: 1px;
border-style: solid;
height: 32px;
@apply flex-grow whitespace-nowrap inline-flex items-center flex-row-reverse;
.align-right {
@apply flex items-center;
}
.right-arrow {
vertical-align: middle;
}
}
}
.page-filter-dropdowngroup {
height: 32px;
color: var(--devui-text, #252b3a);
border-color: var(--devui-line, #d7d8da);
padding: var(--devui-btn-padding, 0 20px);
line-height: var(--devui-line-height-base, 1.5);
border-radius: var(--border-radius);
border-width: 1px;
border-style: solid;
gap: 0 16px;
@apply flex-grow whitespace-nowrap flex flex-row-reverse bg-white;
}
.filter-dropdown-item {
@apply text-CG600 flex justify-between items-center overflow-hidden whitespace-nowrap text-ellipsis w-[72px];
}
.filter-dropdown-key {
@apply overflow-hidden text-ellipsis;
}
.table-box {
width: 100% !important;
:deep(.devui-table__empty) {
text-align: center;
}
:deep(.devui-table__view tbody > tr > td.state-icon) {
padding-right: 0 !important;
vertical-align: top;
}
}
.page-filter-list-header{
flex-wrap: wrap;
}
</style>

View File

@@ -0,0 +1,430 @@
<template>
<div class="box-border">
<Card style="padding:0;">
<div class="org-member">
<div class="tabs flex items-center justify-between">
<div class="tabs-nav flex items-center">
<div v-for="(item) in tabList" :key="item.id" :id="item.id" class="tabs-nav-option"
:class="{ active: tableTab === item.id }" @click="updateTable(item)"><span>{{ item.name }}</span></div>
</div>
<div class="right-search pr-3">
<div v-if="tableTab === 'member'" class="flex items-center">
<d-input v-model="username" placeholder="搜索组织成员" clearable>
<template #prefix>
<Icon name="gt-search" />
</template>
</d-input>
<d-button class="ml-4 flex-shrink-0" variant="solid" color="primary" @click="router.push({ name: 'orgSettingMember' })"
v-if="isAdmin">组织成员管理</d-button>
</div>
<div v-if="tableTab === 'follow'" class="flex items-center">
<d-input v-model="keyword" placeholder="搜索关注用户" clearable @keydown="e=>e.key==='Enter'&&searchFollows()">
<template #prefix>
<Icon name="gt-search" @click="searchFollows"/>
</template>
</d-input>
</div>
</div>
</div>
<div v-if="tableTab === 'member'">
<div class="member">
<DataPanel :loading="loading" :empty="empty" skeleton :card="false" class="my-[12px]">
<MemberItems class="" :data="userList" showTags isManager :isAdmin="isAdmin" @statusChange="handleChange"
@removeMember="handleRemove" @exitProject="handleExit" />
</DataPanel>
</div>
</div>
<div v-if="tableTab === 'follow'">
<div class="overflow-hidden">
<DataPanel :loading="followLoading" :empty="followList.length===0" skeleton :card="false" class="my-[12px]">
<div v-for="item in followList" :key="item.id" class="flex items-center py-4 px-5">
<GAvatar
:src="item.avatar"
:name="item.nickname"
:width="48"
:height="48"
:is_round="true"
></GAvatar>
<div class="flex flex-col ml-5 w-[200px]">
<GText class="text-base">{{ xssPurify(item.nickname) }}</GText>
<div class="text-CG600 text-sm ellipsis mt-2">@{{ item.username }}</div>
</div>
<div class="text-CG600 flex-grow flex justify-center">
<span>{{formatTime(item.create_time, 'YYYY-MM-DD HH:mm:ss') }}</span>
</div>
<div class="w-[100px] flex justify-center">
<template v-if="localUser.username!==item.username">
<d-button v-if="item.is_followed" size="sm" class="follow-btn border boder-G400" @click="toggleFollow(item)">取消关注</d-button>
<d-button v-else size="sm" class="follow-btn border boder-G400" @click="toggleFollow(item)">关注</d-button>
</template>
</div>
</div>
</DataPanel>
</div>
</div>
</div>
</Card>
<div v-if="tableTab === 'member'">
<d-pagination class="px-[20px] py-[20px] flex justify-center" auto-hide size="md" :page-size-options="[10, 20, 50]" :show-page-selector="false"
:can-view-total="true" :can-change-page-size="true" :max-items="5" :total="pager.total"
v-model:pageSize="pager.per_page" v-model:pageIndex="pager.page" @page-index-change="getUsers"
@page-size-change="handleSizeChange" />
</div>
<div v-if="tableTab === 'follow'">
<d-pagination class="px-[20px] py-[20px] flex justify-center" auto-hide size="md" :page-size-options="[10, 20, 50]" :show-page-selector="false"
:can-view-total="true" :can-change-page-size="true" :max-items="5" :total="followPager.total"
v-model:pageSize="followPager.per_page" v-model:pageIndex="followPager.page" @page-index-change="getFollowers()"
@page-size-change="handleFollowSizeChange" />
</div>
<GModal v-model="visible" ref="deleteTip" showWarnIcon @confirm="handleConfirm" :title="warningInfo.title">
<span class="inline-block text-G900 text-sm font-normal leading-[20px] break-all">{{ warningInfo.content }}</span>
</GModal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, toRaw, computed, watch, nextTick, onBeforeUnmount } from 'vue';
import { Message } from 'vue-devui/message';
import MemberItems from '@/components/MemberItems/index.vue';
import type { MemberItemData } from '@/components/MemberItems/types';
import { getOrgMembers, removeGroupUser, exitGroup, setGroupUserLevel } from '@/api/org';
import { getGroupFollows } from '@/api/org/devIndex';
import { useAccountStore } from '@/stores/user';
import { useRouter, useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { orgInfoStore } from '@/stores/Org';
import setting from '@/setting';
import { xssPurify } from '@/utils';
import { GModal } from '@/components/Setting/index';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
import { reqCatchV2 } from '@/utils/catch';
import { followUser, unfollowUser } from '@/api/user';
import { useLoginCheck } from '@/utils/hooks/useLoginCheck';
const route = useRoute();
const router = useRouter();
const { namespace, getUserCreateOrg } = getOrgInfo();
const { visitor, developer, admin } = setting.role;
const loading = ref(true);
const { isAdmin, communityInfo, access_level } = storeToRefs(orgInfoStore());
const { formatTime } = useTimeFormat();
const { loginCheck } = useLoginCheck();
const empty = computed(() => {
return !loading.value && !userList.value?.length;
});
const hasPermission = computed(() => access_level.value >= visitor);
const { accountInfo: localUser } = useAccountStore();
interface RepoGroupUser {
access_level: number | string
email: string
id: number | string
is_current_source_member: boolean
name: string
name_cn?: string
username: string
type: string
web_url: string
[x: string]: any
}
// 邀请成员
const visible = ref(false);
const username = ref('');
const pager = reactive({
page: 1,
per_page: 10,
total: 0
});
const followPager = reactive({
page: 1,
per_page: 10,
total: 0
});
const warningInfo = reactive<{ title: string, content: string, name: string, type: 'remove' | 'exit' | '' }>({
title: '',
content: '',
name: '',
type: ''
});
const tableTab = ref(hasPermission.value ? 'member' : 'follow');
const tabList = hasPermission.value ? [
{
id: 'member',
name: '组织成员'
}, {
id: 'follow',
name: '社区粉丝'
}
] : [{
id: 'follow',
name: '社区粉丝'
}];
const updateTable = (item: any) => {
tableTab.value = item.id;
};
// 判断是否从组织主页banner跳转
const params = new URLSearchParams(window.location.search);
const isOrgToFans = params.get('isOrgToFans');
if (isOrgToFans === 'isOrgToFans') {
const select = tabList.find(v => v.id === 'follow');
select && updateTable(select);
}
watch(() => route.query, (val) => {
if (val?.isOrgToFans === 'isOrgToFans') {
const select = tabList.find(v => v.id === 'follow');
select && updateTable(select);
}
}, { immediate: true, deep: true });
const keyword = ref('');
const followLoading = ref(false);
const followList = ref([]);
const userList = ref<MemberItemData[]>([]);
const getUserInfo = (source: RepoGroupUser, content: RepoGroupUser[]) => {
const userInfo = JSON.parse(localStorage.getItem('userInfo') as string);
const adminList = content?.filter(item => item.access_level.toString() === '50') || [];
return {
imageSrc: source.avatar || source.avatar_url,
nickname: source.nickname || source.username || '-',
userName: source.username,
isSelf: Number(userInfo.arts_id) === Number(source.id),
isManager: source.access_level.toString() >= admin,
isDeveloper: source.access_level.toString() >= developer && source.access_level.toString() < admin,
isVisitor: source.access_level.toString() >= visitor && source.access_level.toString() < developer,
allowedExit: source.access_level.toString() !== '50' || adminList.length > 1,
isApplying: false,
status: source.access_level.toString(),
lastStatus: source.access_level.toString(),
path: `/${source.username}`,
type: source.type,
join_way: source.join_way,
disabledOption: false
};
};
const getUsers = async() => {
const { page, per_page } = pager;
const res = await getOrgMembers({ group_id: namespace.value, page, per_page, query: username.value.trim() });
if (res.status === 200) {
userList.value = (res.data.content as RepoGroupUser[]).map(item => {
return getUserInfo(item, res.data.content);
});
pager.total = res?.data?.total;
pager.page = res?.data?.page_num;
pager.per_page = res?.data?.page_size;
}
loading.value = false;
};
const getFollowers = async() => { // 获取组织的粉丝列表
const { page, per_page } = followPager;
followLoading.value = true;
const res = await getGroupFollows({ groupId: namespace.value, nsId: communityInfo.value?.ns_id, keyword: keyword.value, pageNum: page, pageSize: per_page });
followLoading.value = false;
if (res.data) {
const { content, total } = res.data.data;
followList.value = (content || []).map(({ nick_name, ...other }) => ({ ...other, nickname: nick_name }));
// if (code === 200) {
// followList.value = (data.content || []).map(({ gitCodeUser, createdAt, ...other }) => ({
// ...other,
// avatar: gitCodeUser.avatar,
// nickname: gitCodeUser.nickname,
// username: gitCodeUser.username,
// create_time: createdAt,
// is_followed: gitCodeUser.isFollowedForGitCodeUser
// }));
// followPager.total = data.total;
// }
followPager.total = total;
}
};
const searchFollows = () => {
followPager.page = 1;
followPager.total = 0;
getFollowers();
};
const toggleFollow = async(item:any, key = 'is_followed') => { // 关注用户
if (!loginCheck()) return;
const params = {
username: localUser.username || '',
followType: 0,
unfollowUsername: item.username,
followedUsername: item.username
};
const followRes = await reqCatchV2(() => item[key] ? unfollowUser(params) : followUser(params));
if (followRes.data) {
Message({
type: 'success',
message: `${item[key] ? '取消关注' : '关注'}成功`
});
await getFollowers();
}
};
const handleRemove = async(config: any) => {
const { userName } = config.data;
visible.value = true;
warningInfo.type = 'remove';
warningInfo.name = userName;
warningInfo.title = `你正在移除成员 ${userName}`;
warningInfo.content = '移除成员后无法恢复, 确认移除么?';
};
const handleConfirm = async() => {
const { type, name } = warningInfo;
const config = {
group_id: namespace.value,
username: name
};
const reqFn = type === 'remove'
? removeGroupUser
: exitGroup;
const res = await reqFn(config);
if (!res.error) {
visible.value = false;
Message.success('操作成功');
if (type === 'exit') {
router.replace('/');
} else {
for (const key in warningInfo) {
warningInfo[key as keyof typeof warningInfo] = '';
}
getUsers();
}
}
};
const handleChange = async(data: any) => {
const { status, userName, nickname } = toRaw(data.data);
const cData = await getUserCreateOrg(userName);
if (status === '50' && !cData) {
data.data.status = data.data.lastStatus;
return Message.warning(`${nickname}已经是 5 个组织的管理员,无法再添加为管理员。`);
}
const res = await setGroupUserLevel({ group_id: namespace.value, username: userName, access_level: status });
if (!res.error) {
Message.success('操作成功');
getUsers();
}
};
const handleSearch = async() => {
pager.total = 0;
pager.page = 1;
pager.per_page = 10;
nextTick(() => {
getUsers();
});
};
const setSearch = () => {
const timer = setTimeout(() => {
handleSearch();
}, 200);
const cancel = () => {
clearTimeout(timer);
};
return {
cancel
};
};
const stop = watch(() => username.value.trim(), (_new, _old, onCleanup) => {
const { cancel } = setSearch();
onCleanup(cancel);
});
const handleExit = () => {
visible.value = true;
warningInfo.type = 'exit';
warningInfo.title = `退出组织?`;
warningInfo.content = `你确定你想退出吗?你将无法访问所有存储库和团队。`;
};
const handleSizeChange = () => {
pager.page = 1;
nextTick(() => {
getUsers();
});
};
const handleFollowSizeChange = () => {
followPager.page = 1;
getFollowers();
};
onMounted(() => {
if (hasPermission.value) getUsers();
getFollowers();
});
// watch(() => communityInfo.value, (val) => {
// if (val.ns_id) getFollowers();
// }, { immediate: true });
onBeforeUnmount(() => {
stop();
});
</script>
<style lang="scss" scoped>
.org-member{
.tabs{
height: 60px;
border-bottom: 1px solid var(--color-G300);
.tabs-nav{
.tabs-nav-option{
line-height: 58px;
color: var(--color-CG600);
cursor: pointer;
padding: 0 4px;
margin: 0 12px;
&::after{
content: "";
display: block;
margin: auto;
height: 2px;
width: 0;
background: transparent;
//transition: width var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95)), background-color var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95));
}
&.active{color:var(--color-G900);}
&.active::after{
content: "";
display: block;
margin: auto;
height: 2px;
width: 100%;
background: var(--color-G900);
//transition: width var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95)), background-color var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95));
}
&:hover::after{
content: "";
display: block;
margin: auto;
height: 2px;
width: 100%;
background: var(--color-G900);
//transition: width var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95)),background-color var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95));
}
}
}
}
}
.repo-setting-member-modal {
width: 370px;
box-sizing: border-box;
padding: 0 0 20px;
.devui-modal__body {
padding: 0;
}
.modal-slot-header {
@apply box-border py-[18px] mx-[22px] text-[var(--color-G900)] text-[16px] border-b-[1px] border-solid border-[var(--color-G200)] font-bold;
}
.devui-flexible-overlay {
left: 30px !important;
right: 30px !important;
max-height: 320px;
overflow-y: scroll;
}
}
</style>

View File

@@ -0,0 +1,9 @@
<template>
<MergreFilterList type="organization" :organizationId="namespace"/>
</template>
<script lang="ts" setup>
defineOptions({ name: 'ORG-MR' });
import MergreFilterList from '@/views/Org/Merge/mergList.vue';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { namespace } = getOrgInfo();
</script>

View File

@@ -0,0 +1,696 @@
<template>
<div class="page-filter-list">
<teleport v-if="props.formElem" :to="props.formElem">
<div class="flex">
<SearchBar
:default-keys="keys"
:hiddenNav="type !== 'project'"
:hiddenCreate="!isDeveloperOperate || type !== 'project'"
:hiddenSelect="type === 'personal'"
:labelTotal="labelTotal"
:milestoneTotal="milestoneTotal"
:keyTags="locationSearch"
@tag-delete="removeLocationKey($event)"
@inputChange="(value) => (keys.search = value)"
@select-change="(value) => (keys.scope = value)"
:type-options="[
{ name: '全部', value: 'all' },
{ name: '我创建的', value: 'created_by_me' },
{ name: '分配给我的', value: 'assigned_to_me' },
{ name: '需要我评审的', value: 'need_my_review' },
]"
class="mr-2"
>
<template #create>
<d-button
icon="add"
class="search-bar-create"
@click="create"
variant="solid"
v-if="isDeveloperOperate && type === 'project'"
>新建Pull Request</d-button
>
</template>
</SearchBar>
<div class="flex justify-between gap-4 flex-shrink-0">
<StateSwitchTab
:default-state="keys.state"
:tab-list="tabList"
@active-tab-change="(value) => (keys.state = value)"
>
</StateSwitchTab>
<div class="page-filter-dropdowngroup">
<div class="flex gap-4 items-center">
<!-- 创作者 -->
<d-dropdown
v-if="type === 'project' && isVisitorOperate"
:position="['bottom-start', 'top-start']"
:align="'start'"
:destroy-on-hide="false"
close-scope="blank"
@toggle="(blo:boolean)=> !blo && RefAuthor.clear()"
overlay-class="g-z-index-9999"
>
<d-button variant="text">
<div class="filter-dropdown-item">
创建者
<d-icon name="icon-select-arrow" />
</div>
</d-button>
<template #menu>
<IssueFilterAssignee
title="按创建者过滤"
:repoId="repoId"
:selected-list="[formData.author]"
placeholder="搜索用户"
hidden-tab
@on-option-click="onAuthorOptionClick"
optionClickScope
ref="RefAuthor"
>
</IssueFilterAssignee>
</template>
</d-dropdown>
<span
class="g-mid-line"
v-if="type === 'project' && isVisitorOperate"
></span>
<!-- label -->
<d-dropdown
v-if="type === 'project'"
:position="['bottom-start', 'top-start']"
:align="'start'"
:destroy-on-hide="false"
close-scope="blank"
@toggle="(blo:boolean)=> !blo && RefFilterLabel.clear()"
overlay-class="g-z-index-9999"
>
<d-button variant="text">
<div class="filter-dropdown-item">
label
<d-icon name="icon-select-arrow" />
</div>
</d-button>
<template #menu>
<IssueFilterLabel
title=" Label 过滤"
:target_project_id="repoId"
:selected-list="formData.labels"
placeholder="搜索label"
hidden-tab
@on-option-click="onLabelOptionClick"
ref="RefFilterLabel"
>
</IssueFilterLabel>
</template>
</d-dropdown>
<span class="g-mid-line" v-if="type === 'project'"></span>
<!-- 里程碑 -->
<!-- <d-dropdown v-if="type === 'project'" :position="['bottom-start', 'top-start']" :align="'start'" :destroy-on-hide="false" close-scope="blank"> <d-button variant="text">里程碑<d-icon name="icon-select-arrow" class="right-arrow" /></d-button> <template #menu> <IssueFilterMilestone title="按里程碑过滤" :repoId="repoId" :selected-list="[formData.milestone]" placeholder="搜索里程碑" emptyText="未设置" hidden-tab @on-option-click="onMilestoneOptionClick" optionClickScope> </IssueFilterMilestone> </template> </d-dropdown> -->
<!-- 审视人 -->
<d-dropdown
v-if="type === 'project'"
:position="['bottom-start', 'top-start']"
:align="'start'"
:destroy-on-hide="false"
close-scope="blank"
overlay-class="g-z-index-9999"
>
<d-button variant="text">
<div class="filter-dropdown-item">
评审人
<d-icon name="icon-select-arrow" />
</div>
</d-button>
<template #menu>
<MergeFilterReviewer
title="按评审人过滤"
:repoId="repoId"
:selected-list="formData.reviewers"
placeholder="搜索用户"
hidden-tab
@on-option-click="onReviewOptionClick"
>
</MergeFilterReviewer>
</template>
</d-dropdown>
<span class="g-mid-line" v-if="type === 'project'"></span>
<!-- 合并人 -->
<d-dropdown
v-if="type === 'project' && isFilterAssignee"
:position="['bottom', 'top']"
:destroy-on-hide="false"
close-scope="blank"
overlay-class="g-z-index-9999"
>
<d-button variant="text">
<div class="filter-dropdown-item">
负责人
<d-icon name="icon-select-arrow" />
</div>
</d-button>
<template #menu>
<MergeFilterAssignee
:repoId="repoId"
:selected-list="[formData.assignee]"
hidden-tab
@on-option-click="onAssigneeOptionClick"
optionClickScope
title="按负责人过滤"
placeholder="搜索用户"
>
</MergeFilterAssignee>
</template>
</d-dropdown>
<span
class="g-mid-line"
v-if="type === 'project' && isFilterAssignee"
></span>
<!-- 合并人 -->
<d-dropdown
v-if="type === 'project' && isFilterMerger"
:position="['bottom', 'top']"
:destroy-on-hide="false"
close-scope="blank"
overlay-class="g-z-index-9999"
>
<d-button variant="text">
<div class="filter-dropdown-item">
合并人
<d-icon name="icon-select-arrow" />
</div>
</d-button>
<template #menu>
<MergeFilterAssignee
hidden-tab
:repoId="repoId"
:selected-list="[formData.merger]"
@on-option-click="onmMrgedByOptionClick"
optionClickScope
title="按合并人过滤"
placeholder="搜索用户"
>
</MergeFilterAssignee>
</template>
</d-dropdown>
<span
class="g-mid-line"
v-if="type === 'project' && isFilterMerger"
></span>
<!-- 排序 -->
<d-dropdown
:position="['top-end', 'bottom-end']"
align="start"
:destroy-on-hide="false"
close-scope="all"
overlay-class="g-z-index-9999"
>
<d-button variant="text">
<div class="filter-dropdown-item">
排序
<d-icon name="icon-select-arrow" class="right-arrow" />
</div>
</d-button>
<template #menu>
<CustomOption
v-for="item in sortOption"
:key="item.value"
:selected="sortAndOrder == item.value"
:value="item.value"
@click="setSort(item)"
>{{ item.label }}</CustomOption
>
</template>
</d-dropdown>
</div>
</div>
</div>
</div>
</teleport>
<data-panel :empty="!mergeList[0] && !loading" class="overflow-hidden" :card="false">
<d-table
:show-loading="loading"
:data="mergeList"
:show-header="false"
:row-hovered-highlight="false"
size="lg"
empty="暂无数据"
class="table-box"
>
<!-- state -->
<d-column :width="36" cell-class="state-icon">
<template #default="{ row }">
<!-- mr 状态 -->
<Icon
v-if="row?.state && mrStateOption[row?.state]"
:name="mrStateOption[row?.state].icon"
:color="mrStateOption[row?.state].color"
></Icon>
<d-icon v-else name="icon-branch-merge"></d-icon>
</template>
</d-column>
<!-- info -->
<d-column>
<template #default="{ row }">
<MrBlurb
:projectPath="row.source_project?.path_with_namespace"
:id="row.iid"
:title="row.title"
:stateIcon="mrStateOption[row?.state].icon"
:stateColor="mrStateOption[row?.state].color"
:labels="row.labels"
:idUrl="
$router.resolve({
name: 'repoMergeDetail',
params: {
namespace: row.target_project?.path_with_namespace?.split(
'/'
)[0],
repoName: row.target_project?.path_with_namespace?.split(
'/'
)[1],
mergeId: row.iid,
},
})?.href
"
:author="row.author"
:createdAt="row.created_at"
:updatedAt="row.updated_at"
:milestoneTitle="row.milestone?.title"
:userNotesCount="
row.user_notes_count || row.notes_count?.notes_count
"
:mergeRequestsCount="row.merge_requests_count"
:targetBranch="row.target_branch"
:targetPathWithNamespace="row.target_project?.path_with_namespace"
:sourceBranch="row.source_branch"
:sourceProjectPathWithNamespace="
row.source_project?.path_with_namespace
"
:addedLines="row.added_lines"
:removedLines="row.removed_lines"
/>
</template>
</d-column>
<template #empty>
<d-skeleton :loading="loading">
<template #placeholder>
<div
v-for="i in 1"
:key="i"
class="bg-white py-4 px-4 flex items-center gap-4 my-[-40px]"
>
<SkeletonItem
variant="circle"
style="width: 16px; height: 16px; align-self: flex-start;"
></SkeletonItem>
<div class="flex-grow">
<SkeletonItem style="width: 80%;"></SkeletonItem>
<div class="flex gap-4 mt-2">
<SkeletonItem
v-for="i in 6"
:key="i"
style="width: 50px; height: 16px;"
></SkeletonItem>
</div>
</div>
</div>
</template>
</d-skeleton>
</template>
</d-table>
</data-panel>
<teleport v-if="props.pageElem" :to="props.pageElem">
<d-pagination
auto-hide
size="md"
class="px-[20px] py-[20px] flex justify-center"
:page-size-options="pageOptions.pageSizeOptions"
:show-page-selector="false"
:total="pager.total"
v-model:pageIndex="pager.page"
v-model:pageSize="pager.pageSize"
:can-view-total="true"
:can-change-page-size="true"
:max-items="pageOptions.maxItems"
@page-size-change="pageSizeChange"
:max-page="500"
/>
</teleport>
</div>
</template>
<script lang="ts" setup>
defineOptions({ name: 'MergeFilterLIst' });
import { ref, reactive, onMounted, provide, type Ref, onBeforeMount } from 'vue';
import SearchBar from '@/views/Repo/components/SearchBar/index.vue';
import StateSwitchTab from '@/components/StateSwitchTab/index.vue';
import CustomOption from '@/components/FilterDropDown/CustomOption.vue';
import MergeFilterAssignee from '@/views/Repo/components/FilterDropDown/MergeFilterAssignee.vue';
import IssueFilterLabel from '@/views/Repo/components/FilterDropDown/IssueFilterLabel.vue';
import IssueFilterAssignee from '@/views/Repo/components/FilterDropDown/IssueFilterAssignee.vue';
import MergeFilterReviewer from '@/views/Repo/components/FilterDropDown/MergeFilterReviewer.vue';
import MrBlurb from '@/views/Repo/components/TableItemBlurb/index.vue';
import type { IMilestone } from '@/api/milestone/types';
import type { IAuthor, ILabel } from '@/api/issue/types';
import { mrStateOption } from '@/constant/mr';
import { useSearch } from '@/views/Repo/hooks/useSearch';
import { removeEmptyValue, formatSelectedData } from '@/utils';
import { getRepoMilestones } from '@/api/repo';
import { getProLabels } from '@/api/labels';
import { reqCatch } from '@/utils/catch';
import debounce from 'lodash/debounce';
import { useAccountStore } from '@/stores/user';
const userStore = useAccountStore();
import { emitEvent } from '@/utils/eventBus';
import { useRouter } from 'vue-router';
import {
getMergeRequests,
getMergeListCount,
getMyMergeRequests,
getMyOrgMergeRequests
} from '@/api/merge';
import { escapeResData, pickNickName } from '@/utils';
import { useOrgId } from '@/utils/hooks/useOrgId';
import { repoInfoStore } from '@/stores/Repo';
import { useGlobalInfoStore } from '@/stores/Global';
import isEqual from 'lodash/isEqual';
const { updateMenuNum } = useGlobalInfoStore();
const { isDeveloperOperate, isVisitorOperate } = repoInfoStore();
// 获取组织namespace
const { orgId } = useOrgId('/');
const props = withDefaults(
defineProps<{
repoId?: string;
organizationId?: string;
type: 'organization' | 'project' | 'personal'; // merge 关联主体
searchParams?: Object;
formElem: null|HTMLElement;
pageElem: null|HTMLElement;
filterOption?: string[];
}>(),
{
filterOption: () => [],
formElem: null,
pageElem: null
}
);
const router = useRouter();
const labelTotal = ref(0);
const milestoneTotal = ref(0);
const RefFilterLabel = ref();
const RefAuthor = ref();
const tabList = ref([
{ count: 0, id: 'opened', title: '已开启' },
{ count: 0, id: 'closed', title: '已关闭' },
// { count: 0, id: 'locked', title: '已锁定' },
{ count: 0, id: 'merged', title: '已合并' },
{ count: 0, id: 'all', title: '全部' }
]);
const {
keys,
sortAndOrder,
formData,
locationSearch,
isFilterAssignee,
isFilterMerger,
pager,
pageOptions,
sortOption,
getKeys,
removeLocationKey,
addLocationKey,
clearLocationSearch
} = useSearch({
fetch: () => fetchMergeListData(),
sortOptionType: 'mr',
storageKey: 'mr-' + props?.type,
clientType: 'pc'
});
provide('removeLocationKey', removeLocationKey);
interface ISearch {
scope: string;
search: string;
}
const loading = ref(false);
const mergeList = ref([]);
onBeforeMount(() => {
clearLocationSearch();
});
onMounted(() => {
fetchMergeListData();
props.type === 'project' && getNavCount();
});
const create = () => {
if (userStore.isLogin) {
router.push({ name: 'repoMergeCreate' });
} else {
emitEvent('login');
}
};
const handleSearch = (obj:ISearch) => {
pager.page = 1;
keys.scope = obj.scope;
keys.search = obj.search?.trim();
};
/**
* 获取分页数据 和 数量
*/
const fetchMergeListData = debounce(async() => {
const _keys = getKeys();
const params = {
..._keys,
...props.searchParams,
repoId: props.repoId,
page: pager.page,
per_page: pager.pageSize,
group_id: props.organizationId,
view: 'basic'
};
let fetchApi;
if (props.type === 'project') {
fetchApi = getMergeRequests;
} else if (props.type === 'personal') {
fetchApi = getMyMergeRequests;
} else if (props.type === 'organization') {
fetchApi = getMyOrgMergeRequests;
} else {
fetchApi = getMergeRequests;
}
loading.value = true;
const listRes = await reqCatch(fetchApi, params);
loading.value = false;
const data = escapeResData(listRes);
if (props.type === 'organization') {
const { content = [], total } = data;
const { merge_requests } = content;
mergeList.value = merge_requests;
pager.total = total;
tabList.value.forEach((item) => {
if (typeof content[item.id] === 'number') {
item.count = content[item.id];
}
});
} else if (props.type === 'project') {
let { total = 0, content = [] } = data;
total = total || 0;
content = content || [];
mergeList.value = content;
pager.total = total;
// scope 数据
const pageRes = await reqCatch(getMergeListCount, { ...params, only_count: true });
const tabCount = escapeResData(pageRes);
tabList.value.forEach((item) => {
if (typeof tabCount[item.id] === 'number') {
item.count = tabCount[item.id];
}
});
// 当获取所有已开启mr时更新toolbar数量
const openFilterParams = {
scope: 'all',
state: 'opened',
page: 1,
per_page: 10,
view: 'basic'
};
delete params.repoId;
delete params.group_id;
if (isEqual(openFilterParams, params)) {
updateMenuNum('repoMerge', pager.total);
}
} else if (props.type === 'personal') {
// 当前状态
let { total = 0, content = [] } = data;
total = total || 0;
content = content || [];
mergeList.value = content;
pager.total = total;
// 当前状态数量
const findRes = tabList.value.find((item) => item.id === keys.state);
if (findRes) {
findRes.count = total;
}
// 其他状态数量
const tabs = tabList.value.filter((item) => item.id !== keys.state).map((e) => e.id);
Promise.all(
tabs.map((value) => fetchApi({ ...params, state: value, page: 1, per_page: 1 }))
).then((res) => {
tabList.value.forEach((item) => {
const index = tabs.findIndex((t) => t === item.id);
if (index > -1) {
item.count = res[index].data.total;
}
});
});
}
}, 100);
// 筛选label
const onLabelOptionClick = async(data: ILabel) => {
const selectList = formatSelectedData(data, formData?.labels || [], (a, b) => a.id === b.id);
formData.labels = selectList;
keys.labels = selectList.map((e) => e?.name).join(',');
};
// 创作者
const onAuthorOptionClick = (data: IAuthor) => {
if (formData?.author?.id === data?.id || !data) {
formData.author = null;
keys.author_id = '';
// addLocationSearch()
} else {
formData.author = data;
keys.author_id = data.id;
}
};
// 审视人
const onReviewOptionClick = (data: IAuthor) => {
const selectList = formatSelectedData(data, formData?.reviewers || [], (a, b) => a.id === b.id);
formData.reviewers = selectList;
keys.approval_reviewer_id = selectList.map((e) => e.id).join(',');
};
// 负责人
const onAssigneeOptionClick = (data: IAuthor) => {
if (formData?.assignee?.id === data?.id || !data) {
formData.assignee = null;
keys.assignee_id = '';
} else {
formData.assignee = data;
keys.assignee_id = data.id;
}
};
// 合并人
const onmMrgedByOptionClick = (data: IAuthor) => {
if (formData?.merger?.id === data?.id || !data) {
formData.merger = null;
keys.merged_by = '';
} else {
formData.merger = data;
keys.merged_by = data.id;
}
};
// 筛选里程碑
const onMilestoneOptionClick = async(data: IMilestone) => {
if (formData.milestone_id === data?.id || !data) {
formData.milestone = null;
keys.milestone = '';
} else {
formData.milestone = data;
keys.milestone = data?.name || '';
}
};
// 排序
const setSort = (obj:any) => {
if (sortAndOrder.value === obj.value) {
sortAndOrder.value = '';
keys.sort = '';
keys.order_by = '';
document.body.click();
return;
}
keys.sort = obj.sort;
keys.order_by = obj.order_by;
document.body.click();
};
const pageSizeChange = (size: number) => {
document.body.click();
};
/* 获取labels 和 里程碑数据 */
const getNavCount = async() => {
const resLabel = await getProLabels({ project_id: props.repoId, page: 1, per_page: 1 });
if (!resLabel.error) labelTotal.value = resLabel?.data?.data?.total;
const resMilestone = await reqCatch(getRepoMilestones, {
repoId: props.repoId,
page: 1,
per_page: 1
});
if (!resMilestone.error) milestoneTotal.value = resMilestone?.data?.data?.total;
};
</script>
<style lang="scss" scoped>
.page-filter-list {
display: flex;
flex-direction: column;
gap: 12px 0px;
}
.page-filter-dropdowngroup {
height: 32px;
color: var(--devui-text, #252b3a);
border-color: var(--devui-line, #d7d8da);
padding: var(--devui-btn-padding, 0 20px);
line-height: var(--devui-line-height-base, 1.5);
border-radius: var(--border-radius);
border-width: 1px;
border-style: solid;
gap: 0 16px;
@apply flex-grow whitespace-nowrap flex flex-row-reverse bg-white;
}
.filter-dropdown-item {
@apply text-CG600 flex justify-between items-center overflow-hidden whitespace-nowrap text-ellipsis w-[72px];
}
.filter-dropdown-key {
@apply overflow-hidden text-ellipsis;
}
.table-box {
width: 100% !important;
:deep(.devui-table__empty) {
text-align: center;
}
:deep(.devui-table__view tbody > tr > td.state-icon) {
padding-right: 0 !important;
vertical-align: top;
}
}
</style>

View File

@@ -0,0 +1,226 @@
<!-- 组织列表 -->
<script setup lang="ts">
import { reactive, ref, onMounted, watch, computed } from 'vue';
import RepoItem from '@/components/RepoItem/index.vue';
import { getOrgProjectList } from '@/api/org/index';
import { reqCatch } from '@/utils/catch';
import { useRoute, useRouter } from 'vue-router';
import { dataHandler } from '@/components/RepoItem/datahandle';
import * as types from '@/api/org/types';
import { REPO_TYPES, SORT_TYPES } from '@/views/User/constant/const';
import { storeToRefs } from 'pinia';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { orgInfoStore } from '@/stores/Org';
import { emitEvent } from '@/utils/eventBus';
import { starRepo, unstarRepo } from '@/api/repo';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import debounce from 'lodash/debounce';
const route = useRoute();
const router = useRouter();
const { namespace, languageList, getOrglanguageList } = getOrgInfo();
const orgSetting = reactive<Record<string, any>>({});
const typesAry = ref(REPO_TYPES);
const langsAry = computed(() => {
return languageList.value.map(item => ({
name: item.label,
value: item.label
}));
});
const sortsAry = ref(SORT_TYPES);
const state = reactive<{
repoList: any,
query: any,
}>({
repoList: [],
query: {
search: '',
simple: false
}
});
const { isAdmin } = storeToRefs(orgInfoStore());
// 创建新项目跳转
const naviTo = (name: string, params?: any) => {
// 用于创建项目时组织的自动选择
const { orgInfo } = orgInfoStore();
router.push({ name, params });
};
// 查询项目
watch(() => orgSetting, () => {
pager.pageIndex = 1;
searchRepo();
}, { deep: true, flush: 'post' });
// 获取项目
const repoLoading = ref<boolean>(true);
const pager = reactive({
total: 0,
pageSize: 10,
pageIndex: 1
});
const getOrgProjectListData = async() => {
repoLoading.value = true;
const params: types.commonGroupReqType = {
orgId: namespace.value,
page: pager.pageIndex,
per_page: pager.pageSize,
simple: false,
include_subgroups: true,
search: state.query.search.trim(),
with_programming_language: orgSetting.language,
visibility: orgSetting.type,
order_by: orgSetting.order?.split(':')[0] || '',
sort: orgSetting.order?.split(':')[1] || ''
};
const { data, error } = await reqCatch(getOrgProjectList, params);
if (!error) {
if (data) {
pager.total = data.data.total;
state.repoList = dataHandler(data?.data?.content) as any;
state.repoList.forEach((item, index) => {
Object.assign(item, {
path_with_namespace: data?.data?.content[index].path_with_namespace,
visibility: data?.data?.content[index].visibility
});
});
}
}
repoLoading.value = false;
};
// 每页条数变化
const sizeChangeFun = (size: number) => {
pager.pageIndex = 1;
getOrgProjectListData();
};
const searchRepo = () => {
pager.pageIndex = 1;
getOrgProjectListData();
};
const inputFun = debounce(() => searchRepo(), 500);
const pageIndexChangeFun = () => {
getOrgProjectListData();
};
onMounted(async() => {
getOrgProjectListData();
getOrglanguageList(namespace.value);
});
const { userInfo } = useUserInfo();
const loading = ref(false);
const toggleRepoStar = ({ id, isStar }) => {
if (!userInfo || !userInfo.username) {
emitEvent('login');
return false;
}
if (loading.value) return false;
loading.value = true;
if (isStar) {
unstarRepo({ repoId: id })
.then(() => {
getOrgProjectListData();
}).finally(() => {
loading.value = false;
});
} else {
starRepo({ repoId: id })
.then(() => {
getOrgProjectListData();
}).finally(() => {
loading.value = false;
});
}
};
</script>
<template>
<div class="org-repos">
<div class="org-repos-search mt-[14px]">
<d-form layout="columns" :data="orgSetting">
<d-form-item field="repoName" class="w-full">
<d-input class="search-box flex-1" v-model="state.query.search" placeholder="搜索项目" @input="inputFun">
<template #prefix>
<Icon name="gt-search" />
</template>
</d-input>
</d-form-item>
<d-form-item field="repoDescription">
<d-select class="sel-type horizon-sel" v-model="orgSetting.type" :options="typesAry" placeholder="选择类型" />
</d-form-item>
<d-form-item field="email">
<d-select class="sel-lang horizon-sel" allow-clear v-model="orgSetting.language" :options="langsAry"
placeholder="选择语言" />
</d-form-item>
<d-form-item field="location">
<d-select class="sel-order horizon-sel" allow-clear v-model="orgSetting.order" :options="sortsAry"
placeholder="选择排序" />
</d-form-item>
</d-form>
<d-button v-if="isAdmin" icon="icon-add" variant="solid" color="primary" class="search-bar-create"
@click="naviTo('newRepo')">新建项目</d-button>
</div>
<DataPanel class="org-repos-table g-content-card overflow-hidden" :loading="repoLoading && !state.repoList?.length"
:empty="!state.repoList?.length" :card="false" skeleton>
<div v-loading="repoLoading">
<RepoItem v-for="(item, index) in state.repoList" :key="index" v-bind="item" @handleStar="({isStar}) => item.isStar = isStar">
<template #right>
<canvas height="60"></canvas>
</template>
</RepoItem>
</div>
</DataPanel>
<d-pagination class="px-[20px] py-[20px] flex justify-center" :max-page="500" :page-size-options="[10, 20, 50]"
auto-hide :total="pager.total" v-model:pageSize="pager.pageSize" v-model:pageIndex="pager.pageIndex"
:can-view-total="true" :can-change-page-size="true" @page-index-change="pageIndexChangeFun"
@page-size-change="sizeChangeFun" :max-items="5" />
</div>
</template>
<style lang="scss" scoped>
@import 'devui-theme/styles-var/devui-var.scss';
.org-repos {
&-search {
display: flex;
align-items: center;
justify-content: space-between;
:deep(.devui-form) {
display: flex;
flex: 1;
margin-right: 10px;
}
:deep(.devui-form__label) {
display: none;
}
// .search-box {
// width: 100%;
// }
.horizon-sel {
width: 100px;
margin-left: 10px;
:deep(.devui-select__selection) {
overflow: hidden;
}
}
.create-project {
height: 32px;
padding: 8px 16px;
border-radius: 4px;
background-color: #333;
color: #fff;
}
}
&-table {
margin-top: 12px;
}
}
</style>

View File

@@ -0,0 +1,92 @@
<!-- 组织列表 -->
<script setup lang="ts">
import { ref, watch, computed } from 'vue';
import RepoList from './repoList.vue';
import IssueList from '@/views/Org/Issue/index.vue';
import MergeRequest from '@/views/Org/Merge/index.vue';
const tabList:any[] = [
{
label: '项目',
value: 0
}, {
label: 'issue',
value: 1
}, {
label: 'Pull Request',
value: 2
}
];
const tabActive = ref(0);
const handleTab = (item) => {
tabActive.value = item.value;
};
const tabForm = ref(null);
const pageDeploy = ref(null);
</script>
<template>
<div class="org-repos">
<Card style="padding:0;">
<div>
<div class="tabs flex items-center justify-between">
<div class="tabs-nav flex items-center flex-shrink-0">
<div v-for="tab in tabList" :key="tab.value" class="tabs-nav-option" :class="{active:tab.value === tabActive}" @click="handleTab(tab)"><span>{{tab.label}}</span></div>
</div>
<div class="right-search pr-3">
<div ref="tabForm"></div>
</div>
</div>
<div v-if="tabActive === 0"><RepoList :formElem="tabForm" :pageElem="pageDeploy"/></div>
<div v-if="tabActive === 1"><IssueList :formElem="tabForm" :pageElem="pageDeploy"/></div>
<div v-if="tabActive === 2"><MergeRequest :formElem="tabForm" :pageElem="pageDeploy"/></div>
</div>
</Card>
<div ref="pageDeploy">
</div>
</div>
</template>
<style lang="scss" scoped>
.org-repos{
.tabs{
height: 60px;
border-bottom: 1px solid var(--color-G300);
.tabs-nav{
.tabs-nav-option{
line-height: 58px;
color: var(--color-CG600);
cursor: pointer;
padding: 0 4px;
margin: 0 12px;
&::after{
content: "";
display: block;
margin: auto;
height: 2px;
width: 0;
background: transparent;
// transition: width var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95)),background-color var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95));
}
&.active{color:var(--color-G900);}
&.active::after{
content: "";
display: block;
margin: auto;
height: 2px;
width: 100%;
background: var(--color-G900);
// transition: width var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95)),background-color var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95));
}
&:hover::after{
content: "";
display: block;
margin: auto;
height: 2px;
width: 100%;
background: var(--color-G900);
//transition: width var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95)),background-color var(--devui-animation-duration-slow, .3s) var(--devui-animation-ease-in-out, cubic-bezier(.5, .05, .5, .95));
}
}
}
}
}
</style>

View File

@@ -0,0 +1,248 @@
<!-- 组织列表 -->
<script setup lang="ts">
import { reactive, ref, onMounted, watch, computed } from 'vue';
import RepoItem from '@/components/RepoItem/index.vue';
import { getOrgProjectList } from '@/api/org/index';
import { reqCatch } from '@/utils/catch';
import { useRoute, useRouter } from 'vue-router';
import { dataHandler } from '@/components/RepoItem/datahandle';
import * as types from '@/api/org/types';
import { REPO_TYPES, SORT_TYPES } from '@/views/User/constant/const';
import { storeToRefs } from 'pinia';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { orgInfoStore } from '@/stores/Org';
import { emitEvent } from '@/utils/eventBus';
import { starRepo, unstarRepo } from '@/api/repo';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import debounce from 'lodash/debounce';
const route = useRoute();
const router = useRouter();
const { namespace, languageList, getOrglanguageList } = getOrgInfo();
const orgSetting = reactive<Record<string, any>>({});
const typesAry = ref(REPO_TYPES);
const langsAry = computed(() => {
return languageList.value.map(item => ({
name: item.label,
value: item.label
}));
});
const sortsAry = ref(SORT_TYPES);
const state = reactive<{
repoList: any,
query: any,
}>({
repoList: [],
query: {
search: '',
simple: false
}
});
const props = withDefaults(
defineProps<{
formElem: null|HTMLElement;
pageElem: null|HTMLElement;
}>(),
{ formElem: null, pageElem: null }
);
const { isAdmin } = storeToRefs(orgInfoStore());
// 创建新项目跳转
const naviTo = (name: string, params?: any) => {
const query = {
position: 'org_create'
};
// 用于创建项目时组织的自动选择
const { orgInfo } = orgInfoStore();
router.push({ name, params, query });
};
// 查询项目
watch(() => orgSetting, () => {
pager.pageIndex = 1;
searchRepo();
}, { deep: true, flush: 'post' });
// 获取项目
const repoLoading = ref<boolean>(true);
const pager = reactive({
total: 0,
pageSize: 10,
pageIndex: 1
});
const getOrgProjectListData = async() => {
repoLoading.value = true;
const params: types.commonGroupReqType = {
orgId: namespace.value,
page: pager.pageIndex,
per_page: pager.pageSize,
simple: false,
include_subgroups: true,
search: state.query.search.trim(),
with_programming_language: orgSetting.language,
visibility: orgSetting.type,
order_by: orgSetting.order?.split(':')[0] || '',
sort: orgSetting.order?.split(':')[1] || ''
};
const { data, error } = await reqCatch(getOrgProjectList, params);
repoLoading.value = false;
if (!error) {
if (data) {
pager.total = data.data.total;
state.repoList = dataHandler(data?.data?.content) as any;
state.repoList.forEach((item, index) => {
Object.assign(item, {
path_with_namespace: data?.data?.content[index].path_with_namespace,
visibility: data?.data?.content[index].visibility
});
if (item?.namespace?.parent_id) {
item.title = item.namespace?.name + ' / ' + item.title;
}
});
}
}
};
// 每页条数变化
const sizeChangeFun = (size: number) => {
pager.pageIndex = 1;
getOrgProjectListData();
};
const searchRepo = () => {
pager.pageIndex = 1;
getOrgProjectListData();
};
const inputFun = debounce(() => searchRepo(), 500);
const pageIndexChangeFun = () => {
getOrgProjectListData();
};
onMounted(async() => {
getOrgProjectListData();
getOrglanguageList(namespace.value);
});
const { userInfo } = useUserInfo();
const loading = ref(false);
const toggleRepoStar = ({ id, isStar }) => {
if (!userInfo || !userInfo.username) {
emitEvent('login', { triggerType: 'Star' });
return false;
}
if (loading.value) return false;
loading.value = true;
if (isStar) {
unstarRepo({ repoId: id })
.then(() => {
getOrgProjectListData();
}).finally(() => {
loading.value = false;
});
} else {
starRepo({ repoId: id })
.then(() => {
getOrgProjectListData();
}).finally(() => {
loading.value = false;
});
}
};
const clearFilter = () => {
state.query.search = '';
searchRepo();
};
</script>
<template>
<div class="org-repos">
<teleport v-if="props.formElem" :to="props.formElem">
<div class="org-repos-search">
<d-form layout="columns" :data="orgSetting">
<d-form-item field="repoName">
<d-input class="search-box" :maxlength="100" v-model="state.query.search" placeholder="搜索项目" @input="inputFun">
<template #prefix>
<Icon name="gt-search" />
</template>
<template #suffix>
<span>{{state?.query?.search.length || 0}}/100</span>
<Icon
v-if="state?.query?.search.length"
name="gt-reviewer-pass"
@click="clearFilter"
class="cursor-pointer"
>
</Icon>
</template>
</d-input>
</d-form-item>
<d-form-item field="repoDescription">
<d-select class="sel-type horizon-sel" v-model="orgSetting.type" :options="typesAry" placeholder="选择类型" />
</d-form-item>
<d-form-item field="email">
<d-select class="sel-lang horizon-sel" allow-clear v-model="orgSetting.language" :options="langsAry"
placeholder="选择语言" />
</d-form-item>
<d-form-item field="location">
<d-select class="sel-order horizon-sel" allow-clear v-model="orgSetting.order" :options="sortsAry"
placeholder="选择排序" />
</d-form-item>
</d-form>
<d-button v-if="isAdmin" icon="icon-add" variant="solid" color="primary" class="search-bar-create" @click="naviTo('newRepo')">新建项目</d-button>
</div>
</teleport>
<DataPanel class="org-repos-table overflow-hidden" :loading="repoLoading && !state.repoList?.length"
:empty="!state.repoList?.length" :card="false" skeleton>
<div v-loading="repoLoading">
<RepoItem v-for="(item, index) in state.repoList" :key="index" v-bind="item" @handleStar="({isStar}) => item.isStar = isStar">
<template #right>
<canvas height="60"></canvas>
</template>
</RepoItem>
</div>
</DataPanel>
<teleport v-if="props.pageElem" :to="props.pageElem">
<d-pagination class="px-[20px] py-[20px] flex justify-center" :max-page="500" :page-size-options="[10, 20, 50]"
auto-hide :total="pager.total" v-model:pageSize="pager.pageSize" v-model:pageIndex="pager.pageIndex"
:can-view-total="true" :can-change-page-size="true" @page-index-change="pageIndexChangeFun"
@page-size-change="sizeChangeFun" :max-items="5" />
</teleport>
</div>
</template>
<style lang="scss" scoped>
@import 'devui-theme/styles-var/devui-var.scss';
.org-repos {
&-search {
display: flex;
align-items: center;
justify-content: space-between;
:deep(.devui-form) {
display: flex;
flex: 1;
margin-right: 10px;
}
:deep(.devui-form__label) {
display: none;
}
.horizon-sel {
width: 100px;
margin-left: 10px;
:deep(.devui-select__selection) {
overflow: hidden;
}
}
.create-project {
height: 32px;
padding: 8px 16px;
border-radius: 4px;
background-color: #333;
color: #fff;
}
}
}
</style>

View File

@@ -0,0 +1,254 @@
<template>
<Card v-if="titles?.length > 0">
<div class="actlis">
<div v-for="(item, index) in carouselItems" :key="index" class="actlis-item cursor-pointer hover:text-link"
:class="index != carouselItems.length - 1 ? ' mr-20' : ''" @click="openHref(item.link)">
<div class="actlis-title">
<div>
<GAvatar :src="icons[index]" :width="19" :height="19" />
<span class="actlis-span">{{ titles[index] }}</span>
</div>
<span v-if="titles[index] !== '热门直播'" class="actlis-time"><Time :time="item?.created_at" /></span>
<span v-else class="actlis-time"><Time :time="item?.activity_time" /></span>
</div>
<div class="actlis-imgdiv">
<img class="actlis-img" :src="item?.picture_url" v-if="titles[index] !== '热门文章'" />
<div class="actlis-titdiv actlis-column text-CG800 text-base" v-else>
<div :id="'titref' + index" class="actlis-2line font-bold">{{ item?.title }}</div>
<div class="actlis-3line mt-[16px]" :style="{ '-webkit-line-clamp': lineNums[index] }">{{ item?.desc }}</div>
</div>
</div>
</div>
<!-- <GLink v-if="visitableReport" @click="emit('report')" class="report">举报</GLink> -->
</div>
</Card>
</template>
<script setup lang="ts">
defineOptions({ name: 'activity-list' });
import { ref, useSlots, type StyleValue, computed, nextTick, watch } from 'vue';
const IconActive = new URL('@/assets/imgs/org/icon_active.png', import.meta.url).href;
const IconAi = new URL('@/assets/imgs/org/icon_ai.png', import.meta.url).href;
const IconVideo = new URL('@/assets/imgs/org/icon_video.png', import.meta.url).href;
interface ActiData {
activity_time?: string;
link?: string;
picture_url?: string;
title?: string;
[propName: string]: any;
}
interface IData {
operationContent?: {
activity: ActiData[],
featured: ActiData[],
calendar: ActiData[],
live: ActiData[]
},
visitableReport?: boolean; // 举报 默认可见
}
const lineHeight = (id: string) => {
const el = document.querySelector('#' + id);
if (el && el.clientHeight > 24) {
return 4;
}
return 5;
};
const props = withDefaults(defineProps<IData>(), {
operationContent: () => ({
activity: [],
featured: [],
calendar: []
}),
visitableReport: true
});
// 链接跳转
const openHref = (url?: string) => {
window.open(url, '_blank', '');
};
const slots = useSlots();
const emit = defineEmits<{(e: 'report', /* orgname : string */): void }>();
// const titles = ['最新活动', '热门文章', '热门直播'];
// const icons = [IconActive, IconAi, IconVideo];
const icons = computed(() => {
const iconAry = [];
props.operationContent.featured && props.operationContent.featured.forEach((item, index) => {
if (index < 3) {
iconAry.push(IconAi);
}
});
if (props.operationContent.activity && props.operationContent.activity.length > 0) {
iconAry[0] = IconActive;
}
if (props.operationContent.featured && props.operationContent.featured.length > 0) {
iconAry[1] = IconAi;
}
if (props.operationContent.live && props.operationContent.live.length > 0) {
iconAry[2] = IconVideo;
}
return iconAry.filter(item => item);
});
const titles = computed(() => {
const titAry = [];
props.operationContent.featured && props.operationContent.featured.forEach((item, index) => {
if (index < 3) {
titAry.push('热门文章');
}
});
if (props.operationContent.activity && props.operationContent.activity.length > 0) {
titAry[0] = '最新活动';
}
if (props.operationContent.featured && props.operationContent.featured.length > 0) {
titAry[1] = '热门文章';
}
if (props.operationContent.live && props.operationContent.live.length > 0) {
titAry[2] = '热门直播';
}
return titAry.filter(item => item);
});
const carouselItems = computed(() => {
const dataList = [];
let featIndex: number = 0;
props.operationContent.featured && props.operationContent.featured.forEach((item, index) => {
if (index < 3) {
featIndex = index;
dataList.push(item);
}
});
if (props.operationContent.activity && props.operationContent.activity.length > 0) {
featIndex = 0;
dataList[0] = props.operationContent.activity[0];
}
if (props.operationContent.featured && props.operationContent.featured.length > 1) {
dataList[1] = props.operationContent.featured[featIndex === 0 ? 0 : 1];
}
if (props.operationContent.live && props.operationContent.live.length > 0) {
dataList[2] = props.operationContent.live[0];
}
return dataList.filter(item => item);
});
const lineNums = ref<number[]>([]);
watch(() => carouselItems.value, (val) => {
if (val && val.length > 0) {
nextTick(() => {
val.forEach((item: any, index: number) => {
lineNums.value.push(lineHeight('titref' + index));
});
});
}
}, { deep: true });
</script>
<style lang="scss" scoped>
$imgHeight: 181px;
.actlis {
width: 100%;
// height: 200px;
// border: 1px solid #E6E7E8;
// padding: 22px 0 20px 20px;
display: flex;
// background: #FFFFFF;
.actlis-item {
flex: 1;
width: 0;
height: 100%;
display: flex;
flex-direction: column;
.actlis-title {
display: flex;
align-items: center;
justify-content: space-between;
line-height: 20px;
:deep(.devui-avatar) {
vertical-align: middle;
}
.icon-comment {
font-size: 16px;
}
.actlis-span {
margin-left: 8px;
color: var(--color-CG800);
font-size: var(--text-sm);
font-weight: 500;
}
.actlis-time {
color: var(--color-G700);
font-size: var(--text-sm);
font-weight: 400;
}
}
.actlis-imgdiv {
width: 100%;
flex: 1;
height: 0;
margin-top: 18px;
display: flex;
align-items: center;
justify-content: center;
background: #F8F9FB;
}
.actlis-img {
width: 100%;
height: $imgHeight;
object-fit: cover;
// height: 120px;
}
.actlis-titdiv {
width: 100%;
height: $imgHeight;
padding: 8px;
font-weight: 500;
// display: flex;
// align-items: center;
}
}
}
.actlis-noline {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.actlis-2line {
width: 100%;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; //(行数)
-webkit-box-orient: vertical;
white-space: normal;
word-break: break-all;
}
.actlis-3line {
width: 100%;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 4; //(行数)
-webkit-box-orient: vertical;
white-space: normal;
word-break: break-all;
}
.actlis-column {
flex-direction: column;
// justify-content: center;
}
</style>

View File

@@ -0,0 +1,175 @@
<template>
<div>
<Panel class="selite overflow-hidden" :blank="false">
<template #header>
<div class="selite-lefthead">
<Icon name="gt-comment-c" size="16px" class="mr-20" />
<span class="selite-dspan">社区动态</span>
</div>
</template>
<template #headerRight>
<span class="cursor-pointer" @click="naviTo">
<GIcon name="gt-more-operate" />
</span>
</template>
<DataPanel class="org-repos-table overflow-hidden" :loading="loadingStatus.dynamicLoading" :empty="!comList?.length" :card="false"
skeleton>
<template v-for="(item, index) in comList" :key="item.id">
<div class="selite-content px-[20px] py-[16px]">
<div>
<span class="text-B600 text-base font-medium leading-[24px]">#{{ item.category.category_name
}}</span>
<span class="ml-[8px] text-G900 text-base font-medium leading-[24px]">
<GLink class="info-content__title ellipsis"
:to="{ name: `orgDiscussionDetail`, params: { serialNumber: item.serial_number } }">
{{ item.title }}
</GLink>
</span>
</div>
<div class="mt-[8px]">
<span class="text-CG600 text-sm font-normal leading-[20px]">{{ item.created_by_user_name }}</span>
<span class="ml-[16px] text-CG600 text-sm font-normal leading-[20px]">创建于<Time
:time="item.created_date" /></span>
<GIcon class="ml-[16px]" name="gt-comment" />
<span class="ml-[4px] text-CG600 text-sm font-normal leading-[20px]">{{ item.comment_total }}</span>
<GIcon v-if="item.isAnswered === 1" color="#0EB07B" class="ml-[16px]" name="gt-success" />
<span v-if="item.isAnswered === 1"
class="ml-[4px] text-GN500 text-sm font-normal leading-[20px]">评论已采纳</span>
</div>
</div>
<div class="line"></div>
</template>
</DataPanel>
</Panel>
<d-pagination class=" px-[20px] py-[20px] flex justify-center" :page-size-options="[10, 20, 50]" auto-hide
:total="discussPager.total" v-model:pageSize="discussPager.pageSize" v-model:pageIndex="discussPager.pageIndex"
:can-view-total="true" :can-change-page-size="true" @page-index-change="getDiscussData()"
@page-size-change="sizeChangeFun" :max-items="5" />
<!-- <GLink v-if="visitableReport" @click="emit('report')" class="report">举报</GLink> -->
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'community-dynamics' });
import { ref, reactive, watch, computed } from 'vue';
import { useRouter } from 'vue-router';
import Panel from '@/components/Panel/index.vue';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { namespace, discussData, discussPager, loadingStatus, getDiscussData } = getOrgInfo();
getDiscussData();
const comList = computed(() => {
return discussData.value;
});
const router = useRouter();
// 更多跳转
const naviTo = () => {
router.push({
name: 'orgDiscussion',
params: { namespace: namespace.value }
});
};
// 每页条数变化
const sizeChangeFun = (size: number) => {
discussPager.pageIndex = 1;
getDiscussData();
};
// 监听组织路径变化
// 监听路由后面需要优化成路由监听
let lastNameSpace = namespace.value;
watch(() => namespace.value, (val, oldVal) => {
if (val !== lastNameSpace) {
onUpdate();
}
lastNameSpace = val;
}, {
deep: true
});
// 项目设置后更新
const onUpdate = () => {
getDiscussData();
};
</script>
<style lang="scss" scoped>
.selite {
.selite-lefthead {
line-height: 20px;
display: flex;
align-items: center;
.selite-icon {
margin-right: 21px;
}
.selite-dspan {
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 500;
}
}
.selite-content {
.is-last {
:deep(.g-repo-item) {
border-bottom: none;
}
}
:deep(.g-repo-item) {
border-top: none;
border-left: none;
border-right: none;
border-radius: 0;
box-shadow: none;
}
}
.selite-flex {
display: flex;
align-items: center;
justify-content: center;
}
.line-1 {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.text-B600 {
color: #2562C4;
}
.text-GN500 {
color: #0EB07B;
}
.des-div {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
}
.line {
width: 100%;
height: 1px;
background: #F0F1F2;
}
.info-content__title {
cursor: pointer;
&:hover {
color: var(--color-font);
}
}
}
</style>

View File

@@ -0,0 +1,272 @@
<template>
<Panel class="selite overflow-hidden" :blank="false" v-if="!loadingStatus.activityLoading && activityData?.length">
<template #header>
<div class="selite-lefthead">
<Icon name="gt-calendar-c" size="16px" class="mr-20" />
<span class="selite-dspan">最新活动</span>
</div>
</template>
<template #headerRight>
<span class="cursor-pointer" @click="naviTo">
<GIcon name="gt-more-operate" />
</span>
</template>
<div class="selite-content flex">
<DataPanel :loading="loadingStatus.activityLoading" :empty="!activityData?.length" animation skeleton
class="px-[10px] py-[20px] w-full" :card="false">
<swiper :slidesPerView="'auto'" :spaceBetween="0" :cssMode="true" :navigation="true" :mousewheel="true"
:keyboard="true" :modules="modules" class="mySwiper">
<swiper-slide v-for="item in activityData" :key="item.mediaAid">
<div class="px-[10px] w-[370px] overflow-hidden">
<div class="relative">
<div class="cursor-pointer relative flex items-center justify-center" @click="openHref(item.mediaAid)">
<img :src="item.ext?.coverImg || orgDevimg" class="h-[198px] w-full object-cover" />
<span v-if="!item.ext?.coverImg" class="absolute px-[16px]">{{ item.title || '最新活动' }}</span>
</div>
<div v-if="item.recommend"
class="absolute w-[40px] h-[18px] top-[4px] right-[4px] bg-[#F5AB0B] text-[#FFFFFF] text-xs font-normal leading-[18px] text-center rounded-[2px]">
精选</div>
<!-- <div v-if="item.type === 0 && new Date() < new Date(item.endAt)"
class="absolute w-[40px] h-[18px] top-[4px] right-[4px] bg1 text-[#FFFFFF] text-xs font-normal leading-[18px] text-center rounded-[2px]">
直播</div> -->
<div v-if="item.type === 0 && new Date() <= new Date(item.endAt) && new Date() >= new Date(item.startAt)"
class="flex items-center justify-center absolute w-[56px] h-[18px] top-[4px] left-[4px] bg2 text-[#FFFFFF] text-xs font-normal leading-[18px] text-center rounded-[2px]">
<div class="zb-div"></div>
<span class="ml-[4px]">直播</span>
</div>
<div v-if="false || item.type === 0 && new Date() > new Date(item.endAt)"
class="absolute top-[4px] right-[4px] flex items-center justify-center">
<div
class="bg-[rgba(0,0,0,0.5)] px-[4px] py-[1px] text-[#FFFFFF] text-xs font-normal leading-[16px] rounded-[2px]">
08:35</div>
<div
class="ml-[4px] bg-[rgba(0,0,0,0.5)] px-[4px] py-[1px] text-[#FFFFFF] text-xs font-normal leading-[16px] rounded-[2px]">
23.5万次播放</div>
</div>
</div>
<div class="mt-[16px] text-G900 text-sm font-medium leading-[20px] line-1 w-full">{{ item.title || '-'
}}
</div>
<div class="mt-[4px]">
<span class="text-CG600 text-sm font-normal leading-[20px]">{{ item.user.nickname }}</span>
<span class="ml-[8px] text-CG600 text-sm font-normal leading-[20px]">{{ item.startAt }}</span>
</div>
</div>
</swiper-slide>
<!-- <swiper-slide v-if="repoHandpickLists2.length > 0">
<div class="flex px-[10px] w-full overflow-hidden">
<template v-for="(item, index) in repoHandpickLists2" :key="item.id">
<div class="px-[10px] py-[20px] w-[350px] shrink-0">
<div class="cursor-pointer relative flex items-center justify-center" @click="openHref(item.contentUrl)">
<img :src="item.ext?.coverImg || orgDevimg" class="h-[198px]" />
<span v-if="!item.ext?.coverImg" class="absolute ">{{ item.title || '最新活动' }}</span>
</div>
<div class="mt-[16px] text-G900 text-sm font-medium leading-[20px] line-1 w-full">{{ item.title || '-' }}
</div>
<div class="mt-[4px]">
<span class="text-CG600 text-sm font-normal leading-[20px]">{{ item.user.nickname }}</span>
<span class="ml-[8px] text-CG600 text-sm font-normal leading-[20px]">{{ item.startAt }}</span>
</div>
</div>
</template>
</div>
</swiper-slide>
<swiper-slide v-if="repoHandpickLists3.length > 0">
<div class="flex px-[10px] w-full overflow-hidden">
<template v-for="(item, index) in repoHandpickLists3" :key="item.id">
<div class="px-[10px] py-[20px] w-[350px] shrink-0">
<div class="cursor-pointer relative flex items-center justify-center" @click="openHref(item.contentUrl)">
<img :src="item.ext?.coverImg || orgDevimg" class="h-[198px]" />
<span v-if="!item.ext?.coverImg" class="absolute ">{{ item.title || '最新活动' }}</span>
</div>
<div class="mt-[16px] text-G900 text-sm font-medium leading-[20px] line-1 w-full">{{ item.title || '-' }}
</div>
<div class="mt-[4px]">
<span class="text-CG600 text-sm font-normal leading-[20px]">{{ item.user.nickname }}</span>
<span class="ml-[8px] text-CG600 text-sm font-normal leading-[20px]">{{ item.startAt }}</span>
</div>
</div>
</template>
</div>
</swiper-slide> -->
</swiper>
</DataPanel>
</div>
</Panel>
<!-- <GLink v-if="visitableReport" @click="emit('report')" class="report">举报</GLink> -->
</template>
<script setup lang="ts">
defineOptions({ name: 'dev-new-activity-list' });
import { ref, reactive, useSlots, type StyleValue, watch, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Panel from '@/components/Panel/index.vue';
import RepoItem from '@/components/RepoItem/index.vue';
import RepoSelectModal from '@/views/User/components/repo-select-modal.vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { emitEvent } from '@/utils/eventBus';
import { storeToRefs } from 'pinia';
import { getRepos, starRepo, unstarRepo } from '@/api/repo';
import { dataHandler, useRepoList, type RepoItemData } from '@/views/User/hooks/useRepoList';
import { getOrgHandpickProjectList, getOrgProjectList } from '@/api/org/index';
import { reqCatch } from '@/utils/catch';
import * as types from '@/api/org/types';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { orgInfoStore } from '@/stores/Org';
// Import Swiper Vue.js components
import { Swiper, SwiperSlide } from 'swiper/vue';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
// import required modules
import { Navigation, Pagination, Mousewheel, Keyboard } from 'swiper/modules';
import orgDevimg from '@/assets/imgs/org/org-devimg.png';
const modules = [Navigation, Pagination, Mousewheel, Keyboard];
const baseDevURL = (import.meta as any).env.VITE_DEV_HOST;
// 获取组织path方法
const { namespace, loadingStatus, activityData, getDevActivity } = getOrgInfo();
const { isSelf, userInfo } = useUserInfo();
const route = useRoute();
const { isAdmin, communityInfo } = storeToRefs(orgInfoStore());
// getDevActivity();
// 链接跳转
const openHref = (id?: string) => {
window.open(`/organization/${namespace.value}/${id}.html`, '_blank', '');
};
// 更多跳转
const naviTo = () => {
window.location.assign(`/organization/${namespace.value}/activity`);
};
// 项目设置后更新
const onUpdate = () => {
getDevActivity();
};
// 监听组织路径变化
// 监听路由后面需要优化成路由监听
let lastNameSpace = '';
watch(() => [namespace.value, communityInfo.value], (val, oldVal) => {
const [name, info] = val;
if (name !== lastNameSpace && Object.keys(info).length) {
onUpdate();
lastNameSpace = name;
}
}, {
deep: true,
immediate: true
});
</script>
<style lang="scss" scoped>
.selite {
.selite-lefthead {
line-height: 20px;
display: flex;
align-items: center;
.selite-icon {
margin-right: 21px;
}
.selite-dspan {
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 500;
}
}
.selite-content {
.is-last {
:deep(.g-repo-item) {
border-bottom: none;
}
}
:deep(.g-repo-item) {
border-top: none;
border-left: none;
border-right: none;
border-radius: 0;
box-shadow: none;
}
}
:deep(.swiper-slide) {
width: 370px !important;
}
.selite-flex {
display: flex;
align-items: center;
justify-content: center;
}
.line-1 {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.bg1 {
background: linear-gradient(90deg, #FF6633 0%, #FF4D97 100%);
}
.bg2 {
background: linear-gradient(315deg, #5172FF 0%, #7F98FF 100%);
;
}
.zb-div {
width: 12px;
height: 12px;
background: url('@/assets/imgs/org/dev-liveBroadcast.png') 0 0;
animation: play 1s steps(7) infinite;
}
@keyframes play {
100% {
background-position: -84px;
}
}
:deep(.devui-carousel) {
width: 100%;
}
:deep(.swiper-button-prev:after) {
display: none;
}
:deep(.swiper-button-next:after) {
display: none;
}
:deep(.swiper-button-next) {
border-radius: 50%;
background: url('@/assets/imgs/org/dev-right.png');
background-position: center center;
background-repeat: no-repeat;
background-color: rgba(64, 64, 64, 0.25);
width: 40px;
height: 40px;
backdrop-filter: blur(10px);
pointer-events: auto;
}
:deep(.swiper-button-prev) {
border-radius: 50%;
background: url('@/assets/imgs/org/dev-left.png');
background-position: center center;
background-repeat: no-repeat;
background-color: rgba(64, 64, 64, 0.25);
width: 40px;
height: 40px;
backdrop-filter: blur(10px);
pointer-events: auto;
}
}
</style>

View File

@@ -0,0 +1,256 @@
<template>
<Panel class="selite overflow-hidden" :blank="false" v-if="!loadingStatus.articleLoading && articleData?.length">
<template #header>
<div class="selite-lefthead">
<Icon name="gt-file2-c" size="16px" class="mr-20" />
<span class="selite-dspan">技术文章</span>
</div>
</template>
<template #headerRight>
<span class="cursor-pointer" @click="naviTo">
<GIcon name="gt-more-operate" />
</span>
</template>
<div class="selite-content flex">
<DataPanel :loading="loadingStatus.articleLoading" :empty="!articleData?.length" animation skeleton
class="px-[10px] py-[20px] w-full" :card="false">
<swiper :slidesPerView="'auto'" :spaceBetween="0" :cssMode="true" :navigation="true" :mousewheel="true"
:keyboard="true" :modules="modules" class="mySwiper">
<swiper-slide v-for="item in articleData" :key="item.id">
<div class="px-[10px] w-[370px] overflow-hidden">
<div class="cursor-pointer relative flex items-center justify-center" @click="openHref(item.content.id)">
<img :src="item.content.thumb || orgDevimg" class="h-[198px] w-full object-cover" />
<span v-if="!item.content.thumb" class="absolute cover-tag text-center">#{{
coverTag(item.content.externalData.tags, item.content.name) }}</span>
<div v-if="item.content.top > 0"
class="absolute w-[40px] h-[18px] top-[4px] right-[4px] bg-[#F5AB0B] text-[#FFFFFF] text-xs font-normal leading-[18px] text-center rounded-[2px]">
精选</div>
</div>
<div class="mt-[16px] text-G900 text-sm font-medium leading-[20px] line-1 w-full">{{ item.content.name }}
</div>
<div class="mt-[4px]">
<span class="text-CG600 text-sm font-normal leading-[20px]">{{ item.nickname }}</span>
<span class="ml-[8px] text-CG600 text-sm font-normal leading-[20px]"><Time
:time="item.content.createdTime" /></span>
</div>
</div>
</swiper-slide>
<!-- <swiper-slide v-if="repoHandpickLists2.length > 0">
<div class="flex px-[10px] w-full overflow-hidden">
<template v-for="(item, index) in repoHandpickLists2" :key="item.id">
<div class="px-[10px] py-[20px] w-[350px] shrink-0">
<div class="cursor-pointer relative flex items-center justify-center" @click="openHref(item.content.id)">
<img :src="item.contentUrl || orgDevimg" class="h-[198px]" />
<span v-if="!item.contentUrl" class="absolute ">{{ item.content.externalData.tags[0]?.name || '技术文章' }}</span>
</div>
<div class="mt-[16px] text-G900 text-sm font-medium leading-[20px] line-1 w-full">{{ item.content.desc }}</div>
<div class="mt-[4px]">
<span class="text-CG600 text-sm font-normal leading-[20px]">{{ item.nickname }}</span>
<span class="ml-[8px] text-CG600 text-sm font-normal leading-[20px]"><Time :time="item.content.createdTime" /></span>
</div>
</div>
</template>
</div>
</swiper-slide>
<swiper-slide v-if="repoHandpickLists3.length > 0">
<div class="flex px-[10px] w-full overflow-hidden">
<template v-for="(item, index) in repoHandpickLists3" :key="item.id">
<div class="px-[10px] py-[20px] w-[350px] shrink-0">
<div class="cursor-pointer relative flex items-center justify-center" @click="openHref(item.content.id)">
<img :src="item.contentUrl || orgDevimg" class="h-[198px]" />
<span v-if="!item.contentUrl" class="absolute ">{{ item.content.externalData.tags[0]?.name || '技术文章' }}</span>
</div>
<div class="mt-[16px] text-G900 text-sm font-medium leading-[20px] line-1 w-full">{{ item.content.desc }}</div>
<div class="mt-[4px]">
<span class="text-CG600 text-sm font-normal leading-[20px]">{{ item.nickname }}</span>
<span class="ml-[8px] text-CG600 text-sm font-normal leading-[20px]"><Time :time="item.content.createdTime" /></span>
</div>
</div>
</template>
</div>
</swiper-slide> -->
</swiper>
</DataPanel>
</div>
</Panel>
<!-- <GLink v-if="visitableReport" @click="emit('report')" class="report">举报</GLink> -->
</template>
<script setup lang="ts">
defineOptions({ name: 'dev-technical-article' });
import { ref, reactive, useSlots, type StyleValue, watch, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Panel from '@/components/Panel/index.vue';
import RepoItem from '@/components/RepoItem/index.vue';
import RepoSelectModal from '@/views/User/components/repo-select-modal.vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { emitEvent } from '@/utils/eventBus';
import { storeToRefs } from 'pinia';
import { getRepos, starRepo, unstarRepo } from '@/api/repo';
import { dataHandler, useRepoList, type RepoItemData } from '@/views/User/hooks/useRepoList';
import { getOrgHandpickProjectList, getOrgProjectList } from '@/api/org/index';
import { reqCatch } from '@/utils/catch';
import * as types from '@/api/org/types';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { orgInfoStore } from '@/stores/Org';
// Import Swiper Vue.js components
import { Swiper, SwiperSlide } from 'swiper/vue';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
// import required modules
import { Navigation, Pagination, Mousewheel, Keyboard } from 'swiper/modules';
import orgDevimg from '@/assets/imgs/org/org-devimg.png';
const modules = [Navigation, Pagination, Mousewheel, Keyboard];
const baseDevURL = (import.meta as any).env.VITE_DEV_HOST;
// 获取组织path方法
const { namespace, loadingStatus, articleData, getDevArticle } = getOrgInfo();
const { isSelf, userInfo } = useUserInfo();
const route = useRoute();
const { isAdmin, communityInfo } = storeToRefs(orgInfoStore());
// const swiperAry = computed(() => {
// const a1 = articleData.value?.content.filter((item: any, index: number) => index < 4) || [];
// const a2 = articleData.value?.content.filter((item: any, index: number) => index > 2 && index < 7) || [];
// const a3 = articleData.value?.content.filter((item: any, index: number) => index > 5 && index < 8) || [];
// return [a1, a2, a3];
// });
// getDevArticle();
// 链接跳转
const openHref = (id?: string) => {
window.open(`/organization/${namespace.value}/${id}.html`, '_blank', '');
};
// 更多跳转
const naviTo = () => {
window.location.assign(`/organization/${namespace.value}/article`);
};
// 项目设置后更新
const onUpdate = () => {
getDevArticle();
};
// 监听组织路径变化
// 监听路由后面需要优化成路由监听
let lastNameSpace: any = '';
watch(() => [namespace.value, communityInfo.value], (val, oldVal) => {
const [name, info] = val;
if (name !== lastNameSpace && Object.keys(info).length) {
onUpdate();
lastNameSpace = name;
}
}, {
deep: true,
immediate: true
});
const coverTag = (tags = [], title = '') => {
const str = tags[0]?.name || title || '技术文章';
return /[\u4e00-\u9fa5]+/.test(str) ? str.slice(0, 6) : str;
};
</script>
<style lang="scss" scoped>
.selite {
.selite-lefthead {
line-height: 20px;
display: flex;
align-items: center;
.selite-icon {
margin-right: 21px;
}
.selite-dspan {
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 500;
}
}
.selite-content {
.is-last {
:deep(.g-repo-item) {
border-bottom: none;
}
}
:deep(.g-repo-item) {
border-top: none;
border-left: none;
border-right: none;
border-radius: 0;
box-shadow: none;
}
.cover-tag {
width: fit-content;
height: 70px;
font-size: 50px;
font-weight: bold;
color: #FFFFFF;
line-height: 70px;
white-space: nowrap;
text-overflow: clip;
overflow: hidden;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
}
.selite-flex {
display: flex;
align-items: center;
justify-content: center;
}
:deep(.swiper-slide) {
width: 370px !important;
}
.line-1 {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
:deep(.devui-carousel) {
width: 100%;
}
:deep(.swiper-button-prev:after) {
display: none;
}
:deep(.swiper-button-next:after) {
display: none;
}
:deep(.swiper-button-next) {
border-radius: 50%;
background: url('@/assets/imgs/org/dev-right.png');
background-position: center center;
background-repeat: no-repeat;
background-color: rgba(64, 64, 64, 0.25);
width: 40px;
height: 40px;
backdrop-filter: blur(10px);
pointer-events: auto;
}
:deep(.swiper-button-prev) {
border-radius: 50%;
background: url('@/assets/imgs/org/dev-left.png');
background-position: center center;
background-repeat: no-repeat;
background-color: rgba(64, 64, 64, 0.25);
width: 40px;
height: 40px;
backdrop-filter: blur(10px);
pointer-events: auto;
}
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<div>
<!-- 默认标题 可换插槽 #title-->
<div class="evecal-title" v-if="!slots.title">
<div class="evecal-headerleft">
<Icon name="gt-calendar-c" size="16px" />
<span class="evecal-span">活动日历</span>
</div>
<GLink @click="openMoreHref">
<Icon name="gt-all" color="#707A8"></Icon>
</GLink>
</div>
<slot v-else name="title"></slot>
<!-- 内容 -->
<div class="evecal-body">
<div v-for="(item, index) in calendar" :key="index" class="evecal-content cursor-pointer hover:text-link"
@click="openHref(item)">
<img class="evecal-img" :src="item.picture_url" />
<div class="evecal-infodiv">
<div class="evecal-time">{{ formatTime(item.activity_time, 'YYYY-MM-DD HH:MM') }}</div>
<div class="evecal-activity">{{ item.title }}</div>
</div>
</div>
</div>
<!-- <GLink v-if="visitableReport" @click="emit('report')" class="report">举报</GLink> -->
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'even-calendar' });
import { ref, useSlots, computed, type StyleValue } from 'vue';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { formatTime } = useTimeFormat();
const { namespace } = getOrgInfo();
interface IData {
calendar?: {
activity_time?: string;
link?: string;
picture_url?: string;
title?: string;
[propName: string]: any;
}[],
visitableReport?: boolean; // 举报 默认可见
allUrl?: string
}
const props = withDefaults(defineProps<IData>(), {
calendar: () => [],
visitableReport: true
});
// 链接跳转
const openHref = (item?: any) => {
window.open(`/organization/${namespace.value}/${item.id}.html`, '_blank', '');
};
const openMoreHref = () => {
window.open(`/organization/${namespace.value}/activity`, '_blank', '');
};
const defaultUrl = computed(() => `/organization/${namespace.value}/activity`);
const slots = useSlots();
const emit = defineEmits<{(e: 'report', /* orgname : string */): void }>();
</script>
<style lang="scss" scoped>
.evecal-title {
display: flex;
align-items: center;
justify-content: space-between;
.icon-comment {
font-size: 16px;
}
.evecal-span {
margin-left: 8px;
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 500;
line-height: 1;
}
}
.evecal-headerleft {
display: flex;
}
.evecal-body {
padding-top: 4px;
.evecal-content {
margin-top: 12px;
color: var(--color-CG800);
font-size: var(--text-sm);
font-weight: 400;
cursor: pointer;
display: flex;
.evecal-img {
width: 120px;
height: 68px;
object-fit: cover;
}
.evecal-infodiv {
flex: 1;
margin-left: 16px;
display: flex;
flex-direction: column;
justify-content: center;
.evecal-time {
color: var(--color-CG600);
font-size: var(--text-sm);
font-weight: 400;
}
.evecal-activity {
margin-top: 4px;
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 400;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
}
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<div>
<!-- 默认标题 可换插槽 #title-->
<div class="feacon-title" v-if="!slots.title">
<div class="feacon-headerleft">
<img class="w-3 mr-2" :src="iconPaper" />
<span class="feacon-span">精选内容</span>
</div>
<GLink @click="openMoreHref">
<Icon name="gt-all" color="#707A8"></Icon>
</GLink>
</div>
<slot v-else name="title"></slot>
<!-- 内容 -->
<div class="feacon-body">
<div v-for="(item, index) in featured" :key="index" class="feacon-content cursor-pointer hover:text-link" @click="openHref(item)">
{{ item.title }}
</div>
</div>
<!-- <GLink v-if="visitableReport" @click="emit('report')" class="report">举报</GLink> -->
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'featured-content' });
import { ref, useSlots, computed, type StyleValue } from 'vue';
import { useAssets } from '@/utils/hooks/useRepoInit';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
const { iconPaper } = useAssets();
const { namespace } = getOrgInfo();
interface IData {
featured?: {
activity_time?: string;
link?: string;
picture_url?: string;
title?: string;
[propName: string]: any;
}[],
visitableReport?: boolean; // 举报 默认可见
allUrl?: string
}
withDefaults(defineProps<IData>(), {
featured: () => [],
visitableReport: true
});
// 链接跳转
const openHref = (item?: any) => {
window.open(`/organization/${namespace.value}/${item.id}.html`, '_blank', '');
};
const openMoreHref = () => {
window.open(`/organization/${namespace.value}/article`, '_blank', '');
};
const defaultUrl = computed(() => `/organization/${namespace.value}/article`);
const slots = useSlots();
const emit = defineEmits<{(e: 'report', /* orgname : string */): void }>();
</script>
<style lang="scss" scoped>
.feacon-title {
display: flex;
align-items: center;
justify-content: space-between;
.icon-comment {
font-size: 16px;
}
.feacon-headerleft {
display: flex;
}
.feacon-span {
// margin-left: 8px;
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 500;
}
}
.feacon-body {
padding-top: 4px;
}
.feacon-content {
margin-top: 8px;
color: var(--color-CG800);
font-size: var(--text-sm);
font-weight: 400;
}
.feacon-content:hover {
color: var(--color-link);
}
</style>

View File

@@ -0,0 +1,88 @@
<template>
<div>
<!-- 默认标题 可换插槽 #title-->
<div class="lanlis-title" v-if="!slots.title">
<Icon name="gt-file-code-c" size="16px" />
<span class="lanlis-span">使用语言</span>
</div>
<slot v-else name="title"></slot>
<!-- 语言标签 -->
<div class="lan-list">
<div class="lan-item" v-for="(item, index) in data" :key="index">
<span class="circle" :style="['background-color:' + item.color]"></span>
<span class="name">{{ item.label }}</span>
</div>
</div>
<!-- <GLink v-if="visitableReport" @click="emit('report')" class="report">举报</GLink> -->
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'language-list' });
import { useSlots, type StyleValue } from 'vue';
interface IData {
data?: {
color: StyleValue | undefined; // language color
label: String;// language name
[propName: string]: any;
}[],
visitableReport?: boolean; // 举报 默认可见
}
withDefaults(defineProps<IData>(), {
data: () => [],
visitableReport: true
});
const slots = useSlots();
const emit = defineEmits<{(e: 'report', /* orgname : string */): void }>();
</script>
<style lang="scss" scoped>
.lanlis-title {
display: flex;
align-items: center;
.icon-comment {
font-size: 16px;
}
.lanlis-span {
margin-left: 8px;
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 500;
line-height: 1;
}
}
.lan-list {
display: flex;
flex-wrap: wrap;
margin-top: 4px;
.lan-item {
margin-right: 13px;
margin-top: 8px;
}
}
.circle {
display: inline-block;
width: 4px;
height: 12px;
background-color: #4b4b4b;
margin-right: 8px;
}
.name {
font-size: var(--text-sm);
color: var(--color-G600);
margin-left: 3px;
}
.report {
color: var(--color-G600);
}
</style>

View File

@@ -0,0 +1,82 @@
<template>
<div>
<!-- 默认标题 可换插槽 #title-->
<div class="latact-title" v-if="!slots.title">
<Icon name="gt-activity-c" size="16px" />
<span class="latact-span">最新活动</span>
</div>
<slot v-else name="title"></slot>
<!-- 走马灯活动 -->
<d-carousel height="169px" class="latact-carousel">
<d-carousel-item v-for="(item, index) in activity" :key="index">
<div class="latact-imgdiv cursor-pointer hover:text-link" @click="openHref(item.link)">
<img class="latact-img" :src="item.picture_url" />
</div>
</d-carousel-item>
</d-carousel>
<!-- <GLink v-if="visitableReport" @click="emit('report')" class="report">举报</GLink> -->
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'latest-activity' });
import { ref, useSlots, type StyleValue } from 'vue';
interface IData {
activity?: {
activity_time?: string;
link?: string;
picture_url?: string;
title?: string;
[propName: string]: any;
}[],
visitableReport?: boolean; // 举报 默认可见
}
withDefaults(defineProps<IData>(), {
activity: () => [],
visitableReport: true
});
// 链接跳转
const openHref = (url?: string) => {
window.open(url, '_blank', '');
};
const slots = useSlots();
const emit = defineEmits<{(e: 'report', /* orgname : string */): void }>();
</script>
<style lang="scss" scoped>
.latact-title {
display: flex;
align-items: center;
.icon-comment {
font-size: 16px;
}
.latact-span {
margin-left: 8px;
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 500;
line-height: 1;
}
}
.latact-carousel {
margin-top: 12px;
:deep(.devui-carousel__dots) {
justify-content: flex-end;
}
.latact-imgdiv {
height: 100%;
}
.latact-img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
</style>

View File

@@ -0,0 +1,272 @@
<template>
<div class="org-header">
<Card class="org-header-info " style="padding: 0; border-radius: 0;">
<div class="org-header-body py-[20px] max-w-[1280px] w-full m-auto min-w-sm px-normal">
<!-- left -->
<div class="flex flex-1 w-0 mr-[48px]">
<!-- 头像 -->
<span v-if="!hideAvatar" class="mr-24">
<GAvatar :src="orgData.avatar" :name="orgData.name" :width="100" :height="100" :is_round="false"
:class="!orgData.avatar ? 'avatar' : 'avatar1'">
</GAvatar>
</span>
<!-- 信息 -->
<div class="info-content flex-1 w-0">
<div class="org-name">
<div class="org-namediv max-w-[380px] text-G900 text-xl mr-[20px] font-bold leading-[28px]">
{{ orgData.name }}
</div>
<d-button :variant="orgStore.isFollow ? undefined : 'solid'"
:color="orgStore.isFollow ? 'secondary' : 'primary'" @click="followClickEvent(orgStore.isFollow)"
class="follow-btn" style="height: 28px;transition: none;">{{
orgStore.isFollow ? '已关注' : '关注' }}</d-button>
</div>
<p class="org-description mt-[12px] h-[40px]" :title="orgData.description || '暂无简介'">{{ orgData.description ||
'暂无简介' }}</p>
<div class="org-contact leading-[20px]">
<span class="org-item mr-[17px]" v-if="orgData.location">
<Icon class="org-icon" name="gt-location"></Icon>
<span class="ml-[10px]">{{ orgData.location }}</span>
</span>
<span class="org-item mr-[17px]" v-if="orgData.email">
<Icon class="org-icon" name="gt-mail"></Icon>
<a class="ml-[10px]" :href="orgData.email ? 'mailto:' + orgData.email : undefined">{{ orgData.email }}</a>
</span>
<span class="org-item" v-if="orgData.home_page">
<Icon class="org-icon" name="gt-link"></Icon>
<GLink class="ml-[10px]" :href="orgData.home_page" v-if="orgData.home_page">{{ orgData.home_page }}
</GLink>
</span>
</div>
</div>
</div>
<!-- right -->
<div class="pt-[51px]">
<div class="flex">
<div class="flex" v-for="(item, index) in orgRData" :key="item.label"
@click="handleNav({ key: item.id, url: item.url })">
<GLink :to="getLink(item)" @click.prevent="() => { }"
class="flex flex-col justify-center items-center min-w-[80px]">
<span class="text-G900 text-lg font-medium leading-[21px]">{{ item.num }}</span>
<span class="text-CG600 text-sm font-normal leading-[20px] mt-[8px]">{{ item.label }}</span>
</GLink>
<div v-if="index != orgRData.length - 1" class="linediv mx-[8px]"></div>
</div>
</div>
</div>
</div>
</Card>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'org-header-info' });
import MemberAvatar from '@/components/MemberAvatarList/index.vue';
import { ref, useSlots, watch, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAccountStore } from '@/stores/user';
import { emitEvent } from '@/utils/eventBus';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { orgInfoStore } from '@/stores/Org';
interface userAvatarListRow {
username: string;
iam_id: string;
name?: string;
avatar?: string;
avatar_url?: string;
}
interface IProps {
hideAvatar?: boolean // 隐藏头像
}
const props = defineProps<IProps>();
const slots = useSlots();
// 获取接口方法
const { namespace, getOrgMemberListData, followDevCommunity } = getOrgInfo();
// 获取成员列表
getOrgMemberListData(namespace.value);
// 从仓库获取组织信息
const orgStore = orgInfoStore();
const orgData = computed(() => {
return orgStore.orgInfo || {};
});
// 监听用户权限等级
watch(() => orgData.value.my_role, (val) => {
if (val && val.access_level > 0) {
getOrgMemberListData(namespace.value);
}
}, { deep: true });
const orgRData = computed(() => {
return [
{ label: '社区粉丝', num: orgStore.fansTotal || 0, url: '', id: 'orgMember' },
{ label: '项目', num: orgData.value.project_count || 0, url: '', id: 'orgRepos' },
{ label: '文章', num: orgStore.articleTotal || 0, url: `/organization/${namespace.value}/article` }
// { label: 'star', num: orgData.value.star_count || 0, url: '', id: 'orgRepos' }
];
});
// 成员列表跳转
const router = useRouter();
const naviTo = (name: string, params?: any) => {
router.push({ name, params });
};
// 右侧数字跳转
const route = useRoute();
const getLink = (data: any) => {
try {
return router.resolve({
name: data.id
}).fullPath;
} catch (err) {
// ignore
}
};
// 点击tab跳转路由
const handleNav = (val: any) => {
let name = val.key;
if (!name) {
name = orgStore.communityUrl;
window.open(val.url);
} else {
const query = name === 'orgMember' ? { isOrgToFans: 'isOrgToFans' } : {};
router.push({ name: name, query: query });
}
};
// 获取用户信息
const { userInfo } = useUserInfo();
// 点击关注
// 关注点击
/* 权限验证 */
const account = useAccountStore();
// 关注dev社区
const followClickEvent = (val: boolean) => {
if (!account.isLogin) {
emitEvent('login', { triggerType: '关注组织' });
return false;
}
followDevCommunity(val);
};
</script>
<style lang="scss" scoped>
.org-header {
background: linear-gradient(180deg, #FBFBFB 0%, #FFFFFF 100%);
.org-header-info {
width: 100%;
.org-header-body {
display: flex;
overflow: hidden;
justify-content: space-between;
}
}
}
.orghea-fw-bold {
font-weight: 500;
}
.orghea-lh-32 {
line-height: 32px;
}
.info-content {
.org-name {
display: flex;
// justify-content: space-between;
}
.org-namediv {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.org-namespan {
display: inline-block;
color: var(--color-G900);
font-size: var(--text-2xl);
font-weight: 500;
line-height: 32px;
letter-spacing: 0em;
text-align: left;
}
.org-subtext {
display: inline-block;
height: 20px;
font-size: var(--text-sm);
font-weight: 400;
color: var(--color-CG600);
line-height: 20px;
}
}
.linediv {
width: 1px;
height: 41px;
background: #E4E9F0;
}
.avatar {
:deep(.devui-avatar--style) {
border: 0.5px solid #d3d3d3;
}
}
.avatar1 {
border: 0.5px solid #d3d3d3;
border-radius: 4px;
overflow: hidden;
}
.org-description {
width: 100%;
color: var(--color-CG600);
font-size: var(--text-sm);
font-weight: 400;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
}
.org-contact {
display: flex;
align-items: center;
flex-wrap: wrap;
word-break: break-all;
color: var(--color-CG600);
font-size: var(--text-sm);
font-weight: 400;
.org-item {
line-height: 20px;
display: inline-flex;
align-items: center;
.org-icon {
min-width: 16px;
}
:deep(a) {
color: #707A87;
}
}
}
</style>

View File

@@ -0,0 +1,200 @@
<template>
<Panel class="docinf overflow-hidden" v-if="showReadme">
<template #header>
<div class="docinf-lefthead">
<Icon name="gt-file-c" class="docinf-icon"></Icon>
<span class="docinf-rspan">README.md</span>
</div>
</template>
<template #headerRight>
<span v-if="isAdmin" class="cursor-pointer"
@click="naviTo"><Icon
name="gt-edit"></Icon></span>
</template>
<div ref="eleRef" class="docinf-content" v-loading="readmeLoading">
<MdRender v-model="readmeText"></MdRender >
</div>
</Panel>
<!-- <GLink v-if="visitableReport" @click="emit('report')" class="report">举报</GLink> -->
</template>
<script setup lang="ts">
defineOptions({ name: 'docker-info' });
import { useShowMore } from '@/utils/hooks/useShowMore';
import MdRender from '@/components/MdRender/index.vue';
import { ref, reactive, useSlots, watch, onUnmounted } from 'vue';
import { storeToRefs } from 'pinia';
import Panel from '@/components/Panel/index.vue';
import { useRouter } from 'vue-router';
import { getRepo, getRepoReadme } from '@/api/repo';
import { reqCatch } from '@/utils/catch';
import utf8 from 'crypto-js/enc-utf8';
import Base64 from 'crypto-js/enc-base64';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { orgInfoStore } from '@/stores/Org';
// 获取组织path方法
const { namespace } = getOrgInfo();
interface IData {
// full_path?: string, // 组织路径
visitableReport?: boolean; // 举报 默认可见
}
interface RepoData {
name?: string,
visibility?: string,
readme_url?: string,
tag_count?: number,
watch_count?: number,
forks_count?: number,
star_count?: number,
branch_count?: number,
open_merge_requests_count?: number,
http_url_to_repo?: string,
ssh_url_to_repo?: string,
description?: string,
tag_list?: [],
web_url?: string,
default_branch?: string,
namespace?: {}
[propName: string]: any;
}
const { isAdmin } = storeToRefs(orgInfoStore());
const props = withDefaults(defineProps<IData>(), {
visitableReport: true
});
const eleRef = ref(null);
const { stop } = useShowMore(eleRef);
onUnmounted(() => {
stop && stop();
});
// 监听组织路径变化
// 监听路由后面需要优化成路由监听
let lastNameSpace = namespace.value;
watch(() => namespace.value, (val, oldVal) => {
if (val !== lastNameSpace) {
showReadme.value = false;
readmeText.value = '';
getProInfo();
}
lastNameSpace = val;
}, {
deep: true
});
// 获取rederme文件
const readmeLoading = ref<boolean>(true);
const readmeText = ref<string>('');
const showReadme = ref<boolean>(false);// 是否展示readme
const redUrl = ref<string>();
const getProReadme = async() => {
if (!namespace.value) return;
const proName = namespace.value.split('%2F');
const params = {
repoId: `${namespace.value}%2F${proName[proName.length - 1]}`
};
const { data, error } = await getRepoReadme(params);
readmeLoading.value = false;
if (!error && data?.data) {
readmeText.value = utf8.stringify(Base64.parse(data.data.content || ''));
redUrl.value = data.data.readme_url;
showReadme.value = !!data.data.readme_url;
}
};
getProReadme();
// const slots = useSlots();
// const emit = defineEmits<{(e: 'report', /* orgname : string */): void }>();
const router = useRouter();
const naviTo = () => {
// router.push({ name, params });
if (redUrl.value) {
window.location.assign(redUrl.value);
}
};
const mode = ref('readonly');
</script>
<style lang="scss" scoped>
.docinf {
.docinf-lefthead {
line-height: 20px;
display: flex;
align-items: center;
.docinf-icon {
margin-right: 20px;
}
.docinf-dspan {
color: var(--color-CG600);
font-size: var(--text-sm);
font-weight: 500;
margin-right: 4px;
}
.docinf-rspan {
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 500;
}
}
.docinf-content {
position: relative;
max-height: 400px;
overflow: auto;
&.show-all {
max-height: 100%;
}
&-btn {
position: absolute;
left: 50%;
bottom: 0;
transform: translateX(-50%);
z-index: 2;
width: 100%;
text-align: center;
background: #fff;
&:hover {
cursor: pointer;
}
}
.docinf-section {}
.docinf-section1 {
margin-top: 32px;
}
.docinf-section-title {
color: var(--color-G900);
font-size: var(--text-2xl);
font-weight: 500;
}
.docinf-section-line {
width: 100%;
height: 1px;
background: #F0F1F2;
margin-top: 16px;
}
.docinf-section-content {
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 400;
margin-top: 16px;
}
.docinf-section-origin {
padding-left: 16px;
}
.docinf-section-subtitle {
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 500;
margin-top: 16px;
}
}
}
</style>

View File

@@ -0,0 +1,371 @@
<template>
<div>
<Panel class="selite overflow-hidden" :blank="false">
<template #header>
<div class="selite-lefthead">
<Icon name="gt-folder-c" size="16px" class="mr-20" />
<span class="selite-dspan" v-if="repoParams.repoHandpickList[0]">精选项目</span>
<span class="selite-dspan" v-else-if="popularProjectList[0]">热门项目</span>
<span class="selite-dspan" v-else-if="loadingStatus.reposLoading"></span>
<span class="selite-dspan" v-else>热门项目</span>
</div>
</template>
<template #headerRight>
<repo-select-modal @update="onUpdate" @change="onChange" :setType="'group_project'" :repoParams="repoParams" :hack-req-func="getOrgProjectAllListDataNoPage"
v-if="isAdmin" />
</template>
<div class="selite-content">
<DataPanel v-if="repoParams.repoHandpickList[0]" :loading="loadingStatus.reposLoading" :empty="!repoParams.repoHandpickList?.length" animation skeleton
class="px-[10px] py-[20px] w-full" :card="false">
<swiper :slidesPerView="'auto'" :spaceBetween="0" :cssMode="true" :navigation="true" :mousewheel="true"
:keyboard="true" :modules="modules" class="mySwiper">
<swiper-slide v-for="(item, index) in repoParams.repoHandpickList" :key="item.id">
<div class="flex px-[10px] w-[370px] overflow-hidden swiper-item-repo">
<repo-item class="w-[350px] border-none" :iconHandleList="item.iconHandleList" :id="item.id"
:imgSrc="item.imgSrc" hideStar :title="item.title" :tag-list="item.tagList" :desc="item.desc"
:isStar="item.isStar" :web_url="item.web_url"
:class="{ 'is-last': index === repoHandpickLists1.length - 1 }" @handle-star="({isStar}) => item.isStar = isStar" />
</div>
</swiper-slide>
</swiper>
</DataPanel>
<DataPanel v-else :loading="loadingStatus.reposLoading" :empty="!popularProjectList[0]" animation skeleton
class="px-[10px] py-[20px] w-full" :card="false">
<swiper :slidesPerView="'auto'" :spaceBetween="0" :cssMode="true" :navigation="true" :mousewheel="true"
:keyboard="true" :modules="modules" class="mySwiper">
<swiper-slide v-for="(item, index) in popularProjectList" :key="item.id">
<div class="flex px-[10px] w-[370px] overflow-hidden swiper-item-repo">
<repo-item class="w-[350px] border-none" :iconHandleList="item.iconHandleList" :hideRight="true" :id="item.id"
:imgSrc="item.imgSrc" hideStar :title="item.title" :tag="item.tag" :desc="item.desc"
:isStar="item.isStar" :web_url="item.web_url"
:class="{ 'is-last': index === popularProjectList.length - 1 }" @handle-star="({isStar}) => item.isStar = isStar" />
</div>
</swiper-slide>
</swiper>
</DataPanel>
</div>
</Panel>
</div>
</template>
<script setup lang="ts">
defineOptions({ name: 'selected-items' });
import { ref, reactive, watch, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Panel from '@/components/Panel/index.vue';
import RepoItem, { dataHandler } from '@/components/RepoItem/index.vue';
import RepoSelectModal from '@/views/User/components/repo-select-modal.vue';
import { useUserInfo } from '@/views/User/hooks/useUserInfo';
import { emitEvent } from '@/utils/eventBus';
import { storeToRefs } from 'pinia';
import { starRepo, unstarRepo } from '@/api/repo';
import { getOrgHandpickProjectList, getOrgProjectList } from '@/api/org/index';
import { reqCatch } from '@/utils/catch';
import * as types from '@/api/org/types';
import { getOrgInfo } from '@/views/Org/hooks/orgInfo';
import { orgInfoStore } from '@/stores/Org';
import { useTimeFormat } from '@/utils/hooks/useTimeFormat';
const { formatTimeFromNow } = useTimeFormat();
// Import Swiper Vue.js components
import { Swiper, SwiperSlide } from 'swiper/vue';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
// import required modules
import { Navigation, Pagination, Mousewheel, Keyboard } from 'swiper/modules';
const modules = [Navigation, Pagination, Mousewheel, Keyboard];
// 获取组织path方法
const { namespace, loadingStatus } = getOrgInfo();
// 精选项目
const { isSelf, userInfo } = useUserInfo();
const route = useRoute();
const { isAdmin } = storeToRefs(orgInfoStore());
// 获取组织下精选项目
const repoParams = reactive<{
repoHandpickList: any,
repoAllList: any
}>({
repoHandpickList: [],
repoAllList: [] // 不带分页的所有项目
});
// 热门项目或最新项目
const popularProjectList = ref<RepoItemData[]>([]);
// const repoPageAllList = ref<any>([]);
const getOrgProjectListData = async() => {
const params: types.commonGroupReqType = {
type: 'group_project',
simple: false,
group_id: namespace.value
};
loadingStatus.reposLoading = true;
const { data, error } = await reqCatch(getOrgHandpickProjectList, params);
if (!error) {
if (data) {
const formatData = data?.data?.map((item: any) => {
const allInfo = {
...item.data,
object_id: item.object_id,
resource_id: item.resource_id,
type: item.type
};
const { fork_count, ...newInfo } = allInfo; // 去掉fork数量
return newInfo;
});
repoParams.repoHandpickList = dataHandler(formatData);
repoParams.repoHandpickList.forEach(e => {
e.iconHandleList = e.iconHandleList?.filter(item => item.icon !== 'gt-fork' && item.icon !== 'gt-license'); // 过滤 fork 和 协议
});
if (!formatData[0]) {
await getPopularProjectList();
}
}
}
loadingStatus.reposLoading = false;
};
/**
* 获取最新项目列表
* 当没有精选项目时展示最近的6个项目 支持star筛选时则使用star最多的项目
* 使用star筛选
*/
const getPopularProjectList = async() => {
const result = await reqCatch(getOrgProjectList, {
orgId: namespace.value, page: 1, per_page: 6,
order_by: 'star_count',
simple: false,
visibility: 'public'
});
if (!result.error) {
const { content = [] } = result.data?.data;
popularProjectList.value = content.filter((item) => !!item && (item.id || item.resource_id))
.map((item) => {
const langs = (item.main_repository_language || []).filter((lang) => !!lang);
const lang = langs[0] || '';
const langColor = langs[1] || 'red';
const res = {
id: item.id || item.resource_id || '',
imgSrc: '',
title: item.name || '',
desc: item.description || '暂无简介',
isStar: item.starred || false,
tag: item.visibility,
to: `/${item.path_with_namespace}`,
web_url: `/${item.path_with_namespace}`,
iconHandleList: [
{ icon: 'icon-dot-status', value: lang, type: 'language', iconColor: langColor, label: '', to: '' },
{ icon: 'gt-star', value: item.star_count || 0, label: '', to: '', iconColor: '#707A87' },
// { icon: 'gt-fork', value: item.forks_count || 0, label: '', to: '', iconColor: '#707A87' },
// { icon: 'gt-license', value: item.license?.key, label: '', to: '', iconColor: '#707A87' },
{ icon: 'gt-date', value: formatTimeFromNow(item.updated_at), label: '', to: '', iconColor: '#707A87' }
]
};
if (!lang) {
res.iconHandleList.splice(0, 1);
}
if (!item.license || !item.license?.key) {
res.iconHandleList = res.iconHandleList.filter(item => item.icon !== 'gt-license');
}
return res;
});
}
};
getOrgProjectListData();
const repoHandpickLists1 = computed(() => {
return repoParams.repoHandpickList.filter((item: any, index: number) => index < 4);
});
// 获取组织下所有项目
const pager = reactive({
total: 0,
pageSize: 10,
pageIndex: 1
});
// 监听组织路径变化
// 监听路由后面需要优化成路由监听
let lastNameSpace = namespace.value;
watch(() => namespace.value, (val, oldVal) => {
if (val !== lastNameSpace) {
onUpdate();
}
lastNameSpace = val;
}, {
deep: true
});
// 项目设置后更新
const onUpdate = () => {
getOrgProjectListData();
};
// 点击精选按钮
let pIndex = 1;
const onChange = () => {
pIndex = 1;
getOrgProjectAllListDataNoPage();
};
// 获取项目
const getOrgProjectAllListDataNoPage = async(search?: string, callback?: Function) => {
const params: types.commonGroupReqType = {
search: search,
orgId: namespace.value,
simple: false,
include_subgroups: true,
page: pIndex,
per_page: 9999
};
const { data, error } = await reqCatch(getOrgProjectList, params);
if (!error) {
if (data) {
pager.total = data.data.total;
// Object.assign(orgData, res.data.data);
const formatData = data?.data?.content.map((item: any) => {
return {
...item,
object_id: item.object_id,
resource_id: item.resource_id,
type: item.type
};
});
const ary = dataHandler(formatData);
repoParams.repoAllList = [...ary];
callback?.();
}
}
};
// 处理项目star
const loading = ref(false);
const toggleRepoStar = ({ id, isStar }) => {
if (!userInfo || !userInfo.username) {
emitEvent('login');
return false;
}
if (loading.value) return false;
loading.value = true;
if (isStar) {
unstarRepo({ repoId: id })
.then(() => {
onUpdate();
}).finally(() => {
loading.value = false;
});
} else {
starRepo({ repoId: id })
.then(() => {
onUpdate();
}).finally(() => {
loading.value = false;
});
}
};
</script>
<style lang="scss" scoped>
.selite {
.selite-lefthead {
line-height: 20px;
display: flex;
align-items: center;
.selite-icon {
margin-right: 21px;
}
.selite-dspan {
color: var(--color-G900);
font-size: var(--text-sm);
font-weight: 500;
}
}
.selite-content {
.is-last {
:deep(.g-repo-item) {
border-bottom: none;
}
}
:deep(.g-repo-item) {
border-top: none;
border-left: none;
border-right: none;
border-radius: 0;
box-shadow: none;
.g-repo-item-desc{
height: 60px;
-webkit-line-clamp: 3;
}
}
:deep(.swiper-slide) {
width: 370px !important;
}
}
.selite-flex {
display: flex;
align-items: center;
justify-content: center;
}
:deep(.devui-carousel) {
width: 100%;
}
:deep(.swiper-button-prev:after) {
display: none;
}
:deep(.swiper-button-next:after) {
display: none;
}
:deep(.swiper-button-next) {
border-radius: 50%;
background: url('@/assets/imgs/org/dev-right.png');
background-position: center center;
background-repeat: no-repeat;
background-color: rgba(64, 64, 64, 0.25);
width: 40px;
height: 40px;
backdrop-filter: blur(10px);
pointer-events: auto;
}
:deep(.swiper-button-prev) {
border-radius: 50%;
background: url('@/assets/imgs/org/dev-left.png');
background-position: center center;
background-repeat: no-repeat;
background-color: rgba(64, 64, 64, 0.25);
width: 40px;
height: 40px;
backdrop-filter: blur(10px);
pointer-events: auto;
}
}
.swiper-item-repo {
:deep(.repo-title) {
@apply flex items-center whitespace-nowrap text-ellipsis overflow-hidden;
a {
@apply whitespace-nowrap text-ellipsis overflow-hidden flex-initial;
}
}
:deep(.g-repo-item-desc) {
max-width: none;
}
}
</style>

View File

@@ -0,0 +1,274 @@
import { onMounted, reactive, ref, computed, watch } from 'vue';
import { orgInfoStore, type OrgInfo } from '@/stores/Org/index';
import { reqCatch } from '@/utils/catch';
import { getOrg, getOrgLanguages, getOrgMemberList, checkUserCreateOrg } from '@/api/org/index';
import { joinCommunity, unJoinCommunity, getArticle, getCommunityAttentionStatus, getActivity, getAdvertisement, getLoginState, getArticleAndFansTotle } from '@/api/org/devIndex';
import { discussList } from '@/api/discussion';
import * as types from '@/api/org/types';
import { useAccountStore } from '@/stores/user';
// 封装组织模块公共的方法
export const getOrgInfo = () => {
// 组织数据
const orgStore = orgInfoStore();
// 组织path
const namespace = computed(() => {
return orgStore.orgInfo.full_path;
});
const loadingStatus = reactive<Record<string, boolean>>({
reposLoading: false,
articleLoading: true,
activityLoading: true,
dynamicLoading: false
});
const orgData = ref<OrgInfo>({});
const getOrgData = async(orgName: string) => {
const params = {
orgId: orgName || namespace.value,
moduleSetting: true,
with_full_path: true
};
const { data, error } = await reqCatch(getOrg, params);
if (!error && data) {
orgData.value = data.data;
orgStore.setOrgInfo(orgData.value);
}
};
// 判断用户是否关注组织
const user = useAccountStore();
const isFollow = ref<boolean>(false);
const checkHasFollowedFun = async() => {
const params = {
username: user.accountInfo.username,
otherUsername: orgStore.orgInfo.full_path,
followType: 1 // 0-用户 1-组织
};
const res = await reqCatch(getCommunityAttentionStatus, params);
if (!res.error) {
const { data } = res.data;
isFollow.value = data;
orgStore.setFollow(isFollow.value);
}
};
// 关注社区事件
const followDevCommunity = async(val: boolean) => {
// const { userInfo } = useUserInfo();
// if (!orgStore.communityInfo.ns_id) {
// return Message.warning({
// message: 'devpress社区暂未创建!'
// });
// };
if (orgStore.isDevLogin) {
if (val) {
const params = {
unfollowUsername: namespace.value,
followType: 1
};
const { data, error } = await reqCatch(unJoinCommunity, params);
if (!error) {
isFollow.value = false;
orgStore.setFollow(isFollow.value);
}
} else {
const params = {
followedUsername: namespace.value,
followType: 1
};
const { data, error } = await reqCatch(joinCommunity, params);
if (!error) {
isFollow.value = true;
orgStore.setFollow(isFollow.value);
}
}
}
};
// 获取组织语言
const languageList = ref<{
color: string,
label: string
}[]>([]);
const getOrglanguageList = async(orgName: string) => {
const params: types.commonGroupReqType = {
orgId: orgName || namespace.value
};
const { data, error } = await reqCatch(getOrgLanguages, params);
if (!error && data) {
languageList.value = data.data;
}
};
// 组织成员
const memList = ref<{ name: string, username: string, iam_id: string, avatar_url: string, [p: string]: any }[]>([]);
const memCount = ref(0);
const getOrgMemberListData = async(orgName: string) => {
// 用户权限等级大于0才能查看成员
if (orgData.value.my_role && orgData.value.my_role.access_level > 0) {
const params = {
orgId: orgName || namespace.value
};
const res = await reqCatch(getOrgMemberList, params);
if (!res.error) {
memList.value = res.data?.data.content;
memCount.value = res.data?.data.total || 0;
orgStore.setOrgMember(memCount.value, memList.value);
}
}
};
// 获取组织文章和粉丝数量
const articleTotal = ref<number>(0);
const fansTotal = ref<number>(0);
const getArticleAndFans = async(orgName?: string) => {
const params = {
orgId: orgName || namespace.value
};
const res = await reqCatch(getArticleAndFansTotle, params);
if (!res.error) {
fansTotal.value = res.data?.data.follower_count || 0;
orgStore.setFansTotal(fansTotal.value);
articleTotal.value = res.data?.data.article_count || 0;
orgStore.setArticleTotal(articleTotal.value);
}
};
// 获取dev社区文章 142269
const articleData = ref<{ [p: string]: any }>({ content: [] });
const getDevArticle = async() => {
const params = {
nsId: orgStore.communityInfo.ns_id,
sort: '',
pageNum: 1,
pageSize: 10
};
loadingStatus.articleLoading = true;
const res = await reqCatch(getArticle, params);
if (!res.error) {
articleData.value = res.data.data.data;
}
loadingStatus.articleLoading = false;
};
// 获取社区活动
const activityData = ref<{ [p: string]: any }>({ content: [] });
const getDevActivity = async() => {
const params = {
nsId: orgStore.communityInfo.ns_id,
sort: 'timeAsc',
pageNum: 1,
pageSize: 10
};
loadingStatus.activityLoading = true;
const res = await reqCatch(getActivity, params);
if (!res.error) {
activityData.value = res.data.data.data;
}
loadingStatus.activityLoading = false;
};
// 获取讨论详情
const discussData = ref<{ [p: string]: any }[]>([]);
const discussPager = reactive({
total: 0,
pageSize: 10,
pageIndex: 1
});
const getDiscussData = async(orgName?: string) => {
const params = {
source_id: orgName || orgStore.orgInfo.id,
source_type: 1,
page: discussPager.pageIndex,
size: discussPager.pageSize
};
loadingStatus.dynamicLoading = true;
const res = await reqCatch(discussList, params);
if (!res.error) {
discussPager.total = res.data.data.data.total;
discussData.value = [...res.data.data.data.records];
}
loadingStatus.dynamicLoading = false;
};
// 获取dev运营广告
const advertisementData = ref<{ [p: string]: any }[]>([]);
const getDevAdvertisement = async() => {
const params = {
nsId: orgStore.communityInfo.ns_id
};
const res = await reqCatch(getAdvertisement, params);
if (!res.error) {
advertisementData.value = res.data?.data.data;
orgStore.setAdvertisementData(res.data?.data.data);
}
};
// 判断dev社区是否登录
const isDevLogin = ref<boolean>(false);
const getDevLogin = async() => {
const res = await reqCatch(getLoginState);
if (!res.error) {
isDevLogin.value = res.data?.data.data.isLogin;
orgStore.setDevLogin(true);
}
};
// 获取用户是否可以创建组织 checkUserCreateOrg
const getUserCreateOrg = async(user?: any) => {
const token = localStorage.getItem('access_token');
if (!token) {
return null;
}
const params = {
username: user
};
const res = await reqCatch(checkUserCreateOrg, params);
if (!res.error) {
if (!user) {
localStorage.setItem('group_quota', res.data?.data.group_quota);
localStorage.setItem('manageable_group_num', res.data?.data.manageable_group_num);
orgStore.setIsCreateOrg(res.data?.data.group_quota - res.data?.data.manageable_group_num > 0);
};
}
return res.data?.data.group_quota - res.data?.data.manageable_group_num > 0;
};
// init方法
const init = (orgName: string, userName: string) => {
getOrgData(orgName);
checkHasFollowedFun();
};
return {
orgData,
isFollow,
namespace,
languageList,
memList,
memCount,
articleData,
articleTotal,
fansTotal,
activityData,
discussData,
discussPager,
advertisementData,
isDevLogin,
loadingStatus,
getOrgData,
checkHasFollowedFun,
getOrglanguageList,
getOrgMemberListData,
followDevCommunity,
getDevArticle,
// getDevArticleTotal,
// getDevFansTotal,
getDevActivity,
getDiscussData,
getDevAdvertisement,
getDevLogin,
getArticleAndFans,
getUserCreateOrg,
init
};
};

143
src/views/Org/index.vue Normal file
View File

@@ -0,0 +1,143 @@
<template>
<div class="org">
<div class="org-body w-full">
<SelectedItems />
<DevTechnicalArticle v-if="Object.keys(orgStore.communityInfo).length===0 || orgStore.communityInfo?.ns_id" class="mt-3"/>
<DevNewActivityList v-if="Object.keys(orgStore.communityInfo).length===0 || orgStore.communityInfo?.ns_id" class="mt-3"/>
<!-- 运营位 -->
<div class="mt-3" v-if="orgStore.advertisementData?.length > 0">
<d-carousel
height="100px"
:autoplay="orgStore.advertisementData?.length > 1"
:autoplay-speed="5000"
:arrow-trigger="orgStore.advertisementData?.length > 1?'hover':'never'"
:show-dots="orgStore.advertisementData?.length > 1"
>
<img
v-for="item in orgStore.advertisementData"
@click="openHref(item.link)"
:key="item.id" :src="item.image_pc"
class="w-[1216px] h-[100px] cursor-pointer"
/>
</d-carousel>
</div>
<CommunityDynamics class="mt-3"/>
</div>
</div>
</template>
<script setup lang="ts">
import SelectedItems from '@/views/Org/components/SelectedItems.vue';
import DevTechnicalArticle from '@/views/Org/components/DevTechnicalArticle.vue';
import DevNewActivityList from '@/views/Org/components/DevNewActivityList.vue';
import CommunityDynamics from '@/views/Org/components/CommunityDynamics.vue';
import { ref, reactive, onMounted, onUnmounted, watch, computed } from 'vue';
import { orgInfoStore, type OrgInfo } from '@/stores/Org/index';
// 获取仓库
const orgStore = orgInfoStore();
// 链接跳转
const openHref = (url?: string) => {
window.open(url);
};
// 动画样式头像闪光
const orgImgDivBackground = ref();
let aniTimeOut: any;
let lightTimeOut: any;
function lightAni() {
if (lightTimeOut) clearInterval(lightTimeOut);
let str: number = 0;
let end: number = 10;
let count: number = 0;
lightTimeOut = setInterval(() => {
if (count > 40) {
clearInterval(lightTimeOut);
return;
}
str += 2.5;
end += 2.5;
count++;
orgImgDivBackground.value = {
background: `linear-gradient(135deg, transparent ${0}%, transparent ${str}%, #f1e2e222 ${str}%, #c5f1ea55 ${end}%, transparent ${end}%, transparent ${100}%)`
};
}, 10);
}
watch(() => orgStore.orgNameSpace, (val) => {
fetchInitData();
}, {
deep: true,
flush: 'post'
});
function fetchInitData() {
clearInterval(aniTimeOut);
aniTimeOut = setInterval(() => { lightAni(); }, 6000);
}
onMounted(() => {
fetchInitData();
});
onUnmounted(() => {
aniTimeOut && clearInterval(aniTimeOut);
lightTimeOut && clearInterval(lightTimeOut);
});
</script>
<style lang="scss" scoped>
.org {
.org-header {
height: 48px;
position: relative;
z-index: 1;
.org-top {
position: absolute;
top: 0;
width: 100%;
height: 110px;
z-index: -1;
background: url('../../assets/imgs/org/org-top.png');
}
}
}
.org-body {
// display: flex;
position: relative;
z-index: 2;
.org-left {
width: 300px;
}
.org-right {
flex: 1;
}
}
.org-imgdiv {
width: 160px;
height: 160px;
position: relative;
:deep(img) {
object-fit: cover;
}
}
.org-bg {
position: absolute;
top: 0;
width: 100%;
height: 100%;
}
.follow-btn {
background-color: var(--color-CG100);
color: var(--color-G900);
border: 1px solid var(--color-G400);
padding: 6px 12px;
}
</style>