This commit is contained in:
min.jiang 2024-07-18 20:17:17 +08:00
parent 327b8c5b35
commit 95c845c628
12 changed files with 638 additions and 58 deletions

View File

@ -1,3 +1,4 @@
<!-- eslint-disable vue/valid-v-model -->
<template>
<div class="header-container">
<div class="header-content">
@ -47,18 +48,19 @@
<svg-icon icon="post" class-name="icon" />
{{ $t('Post') }}
</button>
<div class="form-check form-switch header-switch-container">
<div class="form-check form-switch header-switch-container" @click="gotoProfile">
<input
class="form-check-input"
type="checkbox"
role="switch"
:checked="userProfile?.account?.provider?.accepting_request"
id="personal-earning-now-checkbox"
disabled
/>
<label class="form-check-label" for="personal-earning-now-checkbox">
<span>{{ $t('Providing service') }}</span>
</label>
<div class="header-switch-desc">
<div v-if="!userProfile?.account?.provider?.accepting_request" class="header-switch-desc">
{{ $t('Please go to profile page to add money receiving method') }}
</div>
</div>
@ -70,7 +72,10 @@
data-bs-toggle="dropdown"
aria-expanded="false"
id="accountButton"
src="@/assets/profile.png"
:src="userProfile?.account?.basic?.photo?.base64
? userProfile?.account?.basic?.photo?.base64
: profileUrl
"
/>
<ul class="dropdown-menu" aria-labelledby="accountButton">
<li>
@ -95,6 +100,8 @@
<script>
import { UserAuthApi } from '@/utils/backend/index'
import LaguageSwitch from '@/components/LaguageSwitch.vue'
import { UserProfileApi } from '@/utils/index'
import profileUrl from '@/assets/profile.png'
export default {
name: 'HeaderGuest',
@ -108,6 +115,9 @@ export default {
},
unreadRequest() {
return this.$store.getters['basic/unreadRequest']
},
userProfile() {
return this.$store.getters['userProfile/profile']
}
},
created() {
@ -115,9 +125,11 @@ export default {
this.userIdentityNote = this.userIdentityNote.slice(0, 5) + '...'
}
this.$store.dispatch('basic/initWebsocket', this.mnx_getUserAuthToken())
this.fetchProfile()
},
data() {
return {
profileUrl,
userIdentityNote: this.mnx_getUserIdentity(),
activePath: this.$route.meta.activePath
}
@ -157,7 +169,16 @@ export default {
this.mnx_logoutRole()
this.mnx_navToFrontDoor()
},
fetchProfile() {
UserProfileApi.fetchUserProfile()
.then((response) => {
this.$store.dispatch('userProfile/updateProfile', response.data || {})
})
.catch((error) => {
console.log('error', error)
this.mnx_backendErrorHandler(error)
})
},
signout() {
UserAuthApi.signout(this.mnx_getUserIdentity(), this.mnx_getUserAuthToken())
.then((response) => {
@ -289,6 +310,7 @@ export default {
.header-switch-container {
position: relative;
cursor: pointer;
.header-switch-desc {
position: absolute;

View File

@ -208,4 +208,5 @@ export default {
'Some update in your request': 'Some update in your request',
'min(s)': 'min(s)',
'line(s)': 'line(s)',
'Issues management': 'Issues management'
}

View File

@ -190,4 +190,5 @@ export default {
'Some update in your request': 'Some update in your request',
'min(s)': '响应时间',
'line(s)': '每周产出代码',
'Issues management': '问题管理'
}

View File

@ -39,7 +39,9 @@ export default {
mnx_navToNewUserSetFlid(suggested_flid) {
this.$router.push('/new-user-set-flid/' + suggested_flid)
},
mnx_navToProjectIssue(project_id) {
this.$router.push(`/project-issue/${project_id}`)
},
mnx_navToRequestIssue(loadFrom) {
this.$router.push('/request-issue/' + loadFrom)
},

View File

@ -592,18 +592,12 @@ export default {
methods: {
fetchProfile() {
UserProfileApi.fetchUserProfile()
.then((response) => {
this.userProfile = response.data
this.userProfile = this.$store.getters['userProfile/profile']
this.updateLocalIdentityData()
this.updateLocalEmailData()
this.updateLocalMobileData()
this.updateLocalPersonalData()
this.updateLocalPaymentData()
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
},
updateLocalIdentityData() {
this.identityOperation.first_name = this.userProfile.account.basic.first_name
@ -611,6 +605,9 @@ export default {
this.identityOperation.photo.base64 = this.userProfile.account.basic.photo.base64
this.identityOperation.photo.filename = this.userProfile.account.basic.photo.filename
},
updateLocalProfileData() {
this.$store.dispatch('userProfile/updateProfile', this.userProfile || {})
},
//User identity operation
edittingFirstname($event) {
elementHandler.edittingInput($event.target)
@ -646,6 +643,7 @@ export default {
.then((response) => {
this.userProfile.account.basic.photo = response.data.photo
this.updateLocalIdentityData()
this.updateLocalProfileData()
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
@ -885,8 +883,8 @@ export default {
textAreaAujuster.adjustHight(element)
},
updateSelfIntro() {
this.personalOperation.self_intro.content_html =
this.$refs.personal_self_intro_editor_div.innerHTML
// this.personalOperation.self_intro.content_html =
// this.$refs.personal_self_intro_editor_div.innerHTML
if (!this.personalOperation.self_intro.content_html) {
return
}
@ -948,6 +946,8 @@ export default {
if (response.data.accepting_request && response.data.account_link != '') {
window.location = response.data.account_link
}
this.updateLocalProfileData()
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
@ -1144,7 +1144,7 @@ export default {
.btn-group-container {
position: absolute;
right: 32px;
top: 10px;
top: -30px;
}
}
}

View File

@ -82,7 +82,14 @@ export default {
components: { SvgIcon },
name: 'MessageHub',
props: {},
mounted() {},
mounted() {
this.$store.dispatch('basic/updateConversationsPage', {
token: this.mnx_getUserAuthToken(),
cb: error => {
this.mnx_backendErrorHandler(error)
}
})
},
data() {
return {
userIdentityNote: this.mnx_getUserIdentity(),
@ -103,7 +110,7 @@ export default {
this.messages = n_val[0].messages || []
this.clearUnreadMessageBy(n_val[0])
} else {
if (this.selConversation.id === n_val?.[0]?.id) {
if (n_val?.[0] && this.selConversation?.id === n_val?.[0]?.id) {
this.messages = n_val[0].messages || []
this.clearUnreadMessageBy(n_val[0])
}

View File

@ -474,7 +474,18 @@
class="accordion-collapse collapse"
data-bs-parent="#collapse-project-issue"
>
<div class="project-invite-collaborator-containter">
<div class="project-code-git-url-container">
<button
class="project-code-copy-git-url"
@click="gotoIssueManage(project)"
>
{{$t('Issues management')}}
</button>
</div>
<div class="chart-container">
<v-chart :option="currentChartData" autoresize />
</div>
<!-- <div class="project-invite-collaborator-containter">
<button
class="accordion-button collapsed"
type="button"
@ -509,13 +520,10 @@
</button>
</div>
<textarea type="text" v-model="newIssueDescriptions[project_index]" />
<!-- <div class="project-new-issue-action-container">
</div>
</div>
</div> -->
</div>
</div>
</div>
<div>
<!-- <div>
<div
class="project-issue-container"
v-for="(issue, issue_index) in project.project.issue.open_issues"
@ -593,7 +601,7 @@
</div>
</div>
</div>
</div>
</div> -->
</div>
</div>
</div>
@ -700,6 +708,10 @@ export default {
console.error('unexpected value:' + project.status)
}
},
gotoIssueManage(project) {
// requestIssueUtils.fillProjectIssue(project)
this.mnx_navToProjectIssue(project.id)
},
fromIntToProjectStatus(statusInt) {
return convertIntoToProjectStatus(statusInt)
},
@ -1242,6 +1254,7 @@ export default {
@extend .w-20;
}
// ...
.project-issue-bar-container {
@extend .flex-row-container;
@extend .justify-content-between;
@ -1272,6 +1285,7 @@ export default {
text-align: center;
@extend .initiate-button;
}
// ...
.project-milestones-table {
border: 0;

View File

@ -0,0 +1,472 @@
<template>
<div class="workspace-container">
<div class="workspace-body">
<div class="accordion" id="workspace-project-accordion">
<div v-if="project.id" class="accordion-item">
<h2 class="accordion-header">
<button
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapse-project-issue"
aria-expanded="false"
aria-controls="collapse-project-issue"
>
<div class="project-issue-bar-container dashed-container">
<div class="project-issue-open-issues">
<label class="project-item-label">{{ $t('Summary') }}</label>
<p class="project-item-text">{{ project.title }}</p>
</div>
<div class="project-issue-open-issues">
<label class="project-item-label">{{ $t('Open issues') }}</label>
<p class="project-item-text">
{{ project.project.issue.number_of_open_issues }}
</p>
</div>
<div class="project-issue-resolved-issues">
<label class="project-item-label">{{ $t('Resolved issues') }}</label>
<p class="project-item-text">
{{ project.project.issue.number_of_resolved_issues }}
</p>
</div>
<div class="project-issue-closed-issues">
<label class="project-item-label">{{ $t('Closed issues') }}</label>
<p class="project-item-text">
{{ project.project.issue.number_of_closed_issues }}
</p>
</div>
</div>
</button>
</h2>
<div
id="collapse-project-issue"
class="accordion-collapse collapse"
data-bs-parent="#collapse-project-issue"
>
<div class="project-invite-collaborator-containter">
<button
class="accordion-button collapsed"
type="button"
ref="issueAddBtn"
data-bs-toggle="collapse"
data-bs-target="#collapse-project-new-issue"
aria-expanded="false"
aria-controls="collapse-project-issue"
>
<div class="project-invite-collaborator">+ {{ $t('Add Issue') }}</div>
</button>
<div
id="collapse-project-new-issue"
class="accordion-collapse collapse"
data-bs-parent="#collapse-project-new-issue"
>
<div class="project-issue-description-container">
<div class="project-issue-description">
<label class="project-item-label">{{
$t('New issue description')
}}</label>
<button
class="project-issue-description-btn"
@click="
postNewIssue(
project.request.product_id,
project.project_id,
newIssueDescriptions
)
"
>
{{ $t('Submit') }}
</button>
</div>
<textarea ref="issueBtn" type="text" v-model="newIssueDescriptions" />
</div>
</div>
</div>
<div>
<div
class="project-issue-container"
v-for="(issue, issue_index) in project.project.issue.open_issues"
:key="issue_index"
:id="'project-issue-' + issue_index"
>
<div class="accordion-item">
<h2 class="accordion-header">
<button
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
:data-bs-target="'#collapse-project-issue-details' + issue_index"
aria-expanded="false"
aria-controls="collapse-project-issue-details"
>
<div class="project-issue-header dashed-container">
<div class="project-issue-title">
<label class="project-item-label">{{ $t('Issue title') }}</label>
<p class="project-item-text">{{ issue.title }}</p>
</div>
<div class="project-issue-status">
<label class="project-item-label">{{ $t('Status') }}</label>
<p class="project-item-text">
{{ fromIntToProjectIssueStatus(issue.status) }}
</p>
</div>
<div class="project-issue-update">
<label class="project-item-label">{{ $t('Last updated') }}</label>
<p class="project-item-text">
{{ getDateFromFulltimeString(issue.update_time) }}
</p>
</div>
</div>
</button>
</h2>
<div
:id="'collapse-project-issue-details' + issue_index"
class="accordion-collapse collapse"
:data-bs-parent="'#collapse-project-issue-details' + issue_index"
>
<div class="project-issue-description-container">
<div class="project-issue-description">
<label class="project-item-label">{{
$t('Issue description')
}}</label>
<button
:hidden="
!showIssueActionButton(project.project, issue, 'Resolve')
"
class="project-issue-description-btn"
@click="setProjectIssueStatus(issue.id, 1)"
>
{{ $t('Resolve') }}
</button>
<button
:hidden="
!showIssueActionButton(project.project, issue, 'Confirm')
"
class="project-issue-description-btn"
@click="setProjectIssueStatus(issue.id, 2)"
>
{{ $t('Confirm') }}
</button>
<button
:hidden="!showIssueActionButton(project.project, issue, 'Reopen')"
class="project-issue-description-btn"
@click="setProjectIssueStatus(issue.id, 0)"
>
{{ $t('Reopen') }}
</button>
</div>
<p class="project-item-text">{{ issue.description }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { DateUtils, WorksapceApi } from '@/utils/index'
import { projectStatusEnum, convertIntoToProjectIssueStatus, projectIssueStatusEnum } from '@/types/index'
export default {
name: 'projectIssue',
props: {
project_id: {
required: true,
type: String
}
},
data() {
return {
project: {},
newIssueDescriptions: ''
}
},
mounted() {
this.fetchView()
},
methods: {
fetchView() {
WorksapceApi.fetchWorkspaceView()
.then((response) => {
const projects = response.data || []
for (let i = 0; i < projects.length; i ++) {
if (projects[i].id == this.project_id) {
this.project = projects[i]
}
}
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
},
isOngoingProject(project) {
return project.status == projectStatusEnum.ONGOING
},
fromIntToProjectIssueStatus(issueStatus) {
return convertIntoToProjectIssueStatus(issueStatus)
},
getDateFromFulltimeString(fulltime) {
return DateUtils.FromJsonToDateString(fulltime)
},
postNewIssue(product_id, project_id, issue_description) {
if (issue_description == null || issue_description == '') {
alert(this.$t('Please input issue description'))
return
}
WorksapceApi.postIssueForProduct(product_id, project_id, issue_description)
.then((response) => {
this.$refs.issueBtn.blur()
this.$refs.issueAddBtn.click()
this.fetchView()
this.newIssueDescriptions = ''
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
},
setProjectIssueStatus(issue_id, status) {
WorksapceApi.setProductIssueStatus(issue_id, status)
.then((response) => {
this.fetchView()
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
},
showIssueActionButton(project, issue, button_text) {
switch (issue.status) {
case projectIssueStatusEnum.OPEN:
if (button_text === this.$t('Resolve')) {
return project.providers.includes(project.current_user_id)
}
break
case 1:
if (button_text === this.$t('Reopen') || button_text === this.$t('Confirm')) {
return project.issuers.includes(project.current_user_id)
}
break
case projectIssueStatusEnum.CLOSED:
if (button_text === this.$t('Reopen')) {
return project.issuers.includes(project.current_user_id)
}
break
default:
return false
}
},
}
}
</script>
<style lang="scss" scoped>
.workspace-container {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.workspace-header {
@extend .flex-row-container;
@extend .justify-content-end;
}
.workspace-body {
width: 100%;
max-width: $body-width;
padding: 24px 0;
}
.project-issue-bar-container {
@extend .flex-row-container;
@extend .justify-content-between;
}
.project-issue-resolved-issues {
@extend .text-start;
@extend .w-20;
}
.project-issue-open-issues {
@extend .text-start;
@extend .w-20;
}
.project-issue-closed-issues {
@extend .text-start;
@extend .w-20;
}
.project-issue-last-update {
@extend .text-start;
@extend .w-20;
}
.project-add-new-issue {
width: 100%;
text-align: center;
@extend .initiate-button;
}
.project-item-label {
@extend .label-text-light;
}
.project-item-text {
@extend .text-start;
margin-bottom: 0;
font-weight: bold;
}
.project-invite-collaborator-containter {
border-bottom: 1px solid #e1e1e1;
.accordion-button {
// border: 1px solid $primary;
// color: $primary;
// background-color: #F3F6FF;
padding: 12px !important;
&::after {
display: none;
}
}
}
.project-invite-collaborator {
width: 100%;
text-align: center;
@extend .initiate-button;
color: $primary;
background-color: #f3f6ff;
}
.project-invite-collaborator-form-container {
margin: 12px;
height: 37px;
display: flex;
padding: 0 12px;
border-radius: 3px;
align-items: center;
border: 1px solid #e7e8eb;
&:focus-within {
border: 1px solid #1748f8;
}
input {
flex: 1;
margin-right: 12px;
border: none;
outline: none;
box-shadow: none;
}
.project-invite-enter {
width: 16px;
height: 16px;
padding: 3px;
background: #f3f3f5;
border-radius: 3px;
}
}
.project-invite-collaborator-form {
@extend .text-start;
@extend .flex-grow-1;
}
.project-invite-collaborator-input {
width: 100%;
}
.project-invite-collaborator-action-button {
@extend .initiate-button;
@extend .float-end;
}
.project-new-issue-textarea {
height: 100px;
width: 100%;
}
.project-new-issue-action-button {
@extend .initiate-button;
@extend .float-end;
}
.project-new-issue-action-container {
height: 40px;
}
.project-issue-description-container {
padding: 12px;
textarea {
padding: 12px;
border-radius: 12px;
border: 1px solid #e1e1e1;
box-shadow: none;
outline: none;
height: 100px;
width: 100%;
margin-bottom: 12px;
}
}
.project-issue-description {
display: flex;
align-items: center;
margin-bottom: 12px;
label {
flex: 1;
}
.project-issue-description-btn {
@extend .btn;
@extend .btn-link;
padding: 0;
margin-left: 12px;
}
}
.project-issue-action-container {
height: 40px;
}
.project-issue-action-button {
@extend .btn;
@extend .btn-link;
@extend .float-end;
}
.project-issue-statistics-container {
@extend .border;
height: 100px;
@extend .p-3;
}
.project-issue-container {
@extend .justify-content-between;
}
.project-issue-header {
display: flex;
}
.project-issue-title {
@extend .text-start;
@extend .flex-grow-1;
}
.project-issue-status {
@extend .text-start;
@extend .w-10;
}
.project-issue-update {
@extend .text-start;
@extend .w-10;
}
.project-issue-manage-button {
@extend .initiate-button;
@extend .float-end;
}
</style>

View File

@ -23,6 +23,8 @@ import MyWorkspaceRequests from '@/pages/user/workspace/requestManage/Home.vue'
import IssueRequest from '@/pages/user/workspace/requestIssue/Issue.vue'
import RequestSubmitted from '@/pages/user/workspace/requestIssue/Submitted.vue'
import ProjectIssue from '@/pages/user/workspace/projectIssue/Issue.vue'
//Workspace Proposals
import MyWorkspaceProposals from '@/pages/user/workspace/proposalManage/Home.vue'
@ -170,6 +172,13 @@ const router = createRouter({
}
}
},
{
name: 'project-issue',
path: '/project-issue/:project_id',
meta: { requiredRoles: [userRoleEnum.PERSONAL] },
components: { default: ProjectIssue, footer: FooterUser, header: HeaderUser },
props: true
},
{
name: 'request-issue2',
path: '/request-issue/:loadFrom/:requestId',

View File

@ -4,6 +4,7 @@ import { WsConnectionFactory } from '@/utils/backend/websocket'
import { MessageHubApi } from '@/utils/backend/messageHub'
const ignoreEventType = ['test']
let timer = null
const GWT = new Date('01 Jan 1970 00:00:00 GMT').toISOString()
const unreadWorkspaceRules = [
{subject:'request', event:'quoted'},
@ -23,6 +24,45 @@ const checkUnreadRulesBy = (message, rules = []) => {
return false
}
const updateConversations =(state, {token ,cb}, data) => {
MessageHubApi.fetchConversations(GWT, token).then((response) => {
const conversations = response.data.conversations || []
let updateLength = 0
if (conversations.length === 0) {
state.unreadConversationCount = 0
localStorage.setItem('unreadConversationCount', 0)
}
if (data && data.event !== 'connected') {
updateLength = conversations.length - state.conversations.length
if (updateLength === 0) updateLength = 1
for (let i = 0; i < updateLength; i++) {
conversations[i]['unread'] = true
}
}
state.conversations = conversations
localStorage.setItem('conversations', JSON.stringify(conversations))
const conversation = conversations[0]
if (conversation?.id) {
MessageHubApi.fetchMessages(
conversation.id,
conversation.message_update_time ? conversation.message_update_time : GWT,
token
).then((response) => {
conversations[0].messages = response?.data || []
conversations[0].message_update_time = new Date().toISOString()
// state.conversations = conversations
state.unreadConversationCount = updateLength
// localStorage.setItem('conversations', JSON.stringify(conversations))
localStorage.setItem('unreadConversationCount', updateLength)
})
}
}).catch(err => {
if (cb && cb instanceof Function) {
cb(err)
}
})
}
const basicStore = {
namespaced: true,
state() {
@ -46,7 +86,7 @@ const basicStore = {
token,
() => {
// keep
setInterval(() => {
timer = setInterval(() => {
state.downstream_web_socket.send('keep alive')
}, 1000 * 60)
console.log('downstream_web_socket open')
@ -80,39 +120,16 @@ const basicStore = {
state.unreadWorkspace = true
}
MessageHubApi.fetchConversations(GWT, token).then((response) => {
const conversations = response.data.conversations || []
let updateLength = 0
if (data.event !== 'connected') {
updateLength = conversations.length - state.conversations.length
if (updateLength === 0) updateLength = 1
for (let i = 0; i < updateLength; i++) {
conversations[i]['unread'] = true
}
}
const conversation = conversations[0]
if (conversation?.id) {
MessageHubApi.fetchMessages(
conversation.id,
conversation.message_update_time ? conversation.message_update_time : GWT,
token
).then((response) => {
conversations[0].messages = response?.data || []
conversations[0].message_update_time = new Date().toISOString()
state.conversations = conversations
state.unreadConversationCount = updateLength
localStorage.setItem('conversations', JSON.stringify(conversations))
localStorage.setItem('unreadConversationCount', updateLength)
})
}
})
updateConversations(state,{token},data)
},
() => {
console.log('downstream_web_socket error')
},
() => {
console.log('downstream_web_socket closed')
if (timer) {
clearInterval(timer)
}
}
)
},
@ -128,6 +145,9 @@ const basicStore = {
}
}
},
updateConversationsPage(state, token) {
updateConversations(state, token)
},
clearUnreadWorkspace(state) {
state.unreadWorkspace = false
},
@ -145,6 +165,9 @@ const basicStore = {
readMessageBy(context, sender) {
context.commit('readMessageBy', sender)
},
updateConversationsPage(context, {token, cb}) {
context.commit('updateConversationsPage', {token, cb})
},
clearUnreadWorkspace(context) {
context.commit('clearUnreadWorkspace')
},

View File

@ -4,7 +4,8 @@ const userProfileStore = {
namespaced: true,
state() {
return {
role: userRoleEnum.NONE
role: userRoleEnum.NONE,
profile: {}
}
},
mutations: {
@ -16,6 +17,18 @@ const userProfileStore = {
if (localStorage.role) {
state.role = Number(localStorage.role)
}
},
setProfile(state, profile) {
state.profile = profile
localStorage.setItem('profile', JSON.stringify(profile || {}))
},
loadProfile(state) {
try {
const pj = localStorage.getItem('profile')
state.profile = JSON.parse(pj)
} catch (error) {
console.log('err', error)
}
}
},
actions: {
@ -24,14 +37,22 @@ const userProfileStore = {
},
logoutRoles(context) {
context.commit('setRole', { role: userRoleEnum.NONE })
context.commit('setProfile', {})
},
loadRoleLocal(context) {
context.commit('loadRole')
context.commit('loadProfile')
},
updateProfile(context, profile) {
context.commit('setProfile', profile)
}
},
getters: {
userRole(state) {
return state.role
},
profile(state) {
return state.profile
}
}
}

View File

@ -13,6 +13,14 @@ class RequestIssueUtils {
return this.drafted_request
}
fillProjectIssue(issue) {
this.projectIssue = issue
}
fetchProjectIssue() {
return this.projectIssue
}
fillTemplatedRequest(request) {
this.templated_request = request
}