搜索结果列表页面开发
This commit is contained in:
420
src/views/Org/Create/index.vue
Normal file
420
src/views/Org/Create/index.vue
Normal 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>
|
||||
14
src/views/Org/Discussion/Create/index.vue
Normal file
14
src/views/Org/Discussion/Create/index.vue
Normal 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>
|
||||
13
src/views/Org/Discussion/Detail/index.vue
Normal file
13
src/views/Org/Discussion/Detail/index.vue
Normal 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>
|
||||
13
src/views/Org/Discussion/SectionCreate/index.vue
Normal file
13
src/views/Org/Discussion/SectionCreate/index.vue
Normal 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>
|
||||
13
src/views/Org/Discussion/Select/index.vue
Normal file
13
src/views/Org/Discussion/Select/index.vue
Normal 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>
|
||||
13
src/views/Org/Discussion/TypeCreate/index.vue
Normal file
13
src/views/Org/Discussion/TypeCreate/index.vue
Normal 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>
|
||||
13
src/views/Org/Discussion/TypeManage/index.vue
Normal file
13
src/views/Org/Discussion/TypeManage/index.vue
Normal 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>
|
||||
11
src/views/Org/Discussion/index.vue
Normal file
11
src/views/Org/Discussion/index.vue
Normal 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>
|
||||
9
src/views/Org/Issue/index.vue
Normal file
9
src/views/Org/Issue/index.vue
Normal 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>
|
||||
521
src/views/Org/Issue/issueList.vue
Normal file
521
src/views/Org/Issue/issueList.vue
Normal 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>
|
||||
|
||||
430
src/views/Org/Member/index.vue
Normal file
430
src/views/Org/Member/index.vue
Normal 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>
|
||||
9
src/views/Org/Merge/index.vue
Normal file
9
src/views/Org/Merge/index.vue
Normal 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>
|
||||
696
src/views/Org/Merge/mergList.vue
Normal file
696
src/views/Org/Merge/mergList.vue
Normal 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>
|
||||
|
||||
226
src/views/Org/Repos/index copy.vue
Normal file
226
src/views/Org/Repos/index copy.vue
Normal 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>
|
||||
92
src/views/Org/Repos/index.vue
Normal file
92
src/views/Org/Repos/index.vue
Normal 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>
|
||||
248
src/views/Org/Repos/repoList.vue
Normal file
248
src/views/Org/Repos/repoList.vue
Normal 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>
|
||||
254
src/views/Org/components/ActivityList.vue
Normal file
254
src/views/Org/components/ActivityList.vue
Normal 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>
|
||||
175
src/views/Org/components/CommunityDynamics.vue
Normal file
175
src/views/Org/components/CommunityDynamics.vue
Normal 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>
|
||||
|
||||
272
src/views/Org/components/DevNewActivityList.vue
Normal file
272
src/views/Org/components/DevNewActivityList.vue
Normal 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>
|
||||
256
src/views/Org/components/DevTechnicalArticle.vue
Normal file
256
src/views/Org/components/DevTechnicalArticle.vue
Normal 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>
|
||||
133
src/views/Org/components/EventCalendar.vue
Normal file
133
src/views/Org/components/EventCalendar.vue
Normal 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>
|
||||
97
src/views/Org/components/FeaturedContent.vue
Normal file
97
src/views/Org/components/FeaturedContent.vue
Normal 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>
|
||||
88
src/views/Org/components/LanguageList.vue
Normal file
88
src/views/Org/components/LanguageList.vue
Normal 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>
|
||||
82
src/views/Org/components/LatestActivity.vue
Normal file
82
src/views/Org/components/LatestActivity.vue
Normal 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>
|
||||
272
src/views/Org/components/OrgHeaderInfo.vue
Normal file
272
src/views/Org/components/OrgHeaderInfo.vue
Normal 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>
|
||||
200
src/views/Org/components/ReadmeInfo.vue
Normal file
200
src/views/Org/components/ReadmeInfo.vue
Normal 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>
|
||||
371
src/views/Org/components/SelectedItems.vue
Normal file
371
src/views/Org/components/SelectedItems.vue
Normal 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>
|
||||
274
src/views/Org/hooks/orgInfo.ts
Normal file
274
src/views/Org/hooks/orgInfo.ts
Normal 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
143
src/views/Org/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user