Merge and resolve conflict

This commit is contained in:
jetli 2024-07-04 21:11:38 +08:00
commit b98e2b8c16
32 changed files with 1052 additions and 424 deletions

View File

@ -14,9 +14,11 @@
"axios": "^1.4.0", "axios": "^1.4.0",
"bootstrap": "^5.3.1", "bootstrap": "^5.3.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"echarts": "^5.5.1",
"pdfjs-dist": "^4.3.136", "pdfjs-dist": "^4.3.136",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-echarts": "^6.7.3",
"vue-i18n": "^9.13.1", "vue-i18n": "^9.13.1",
"vue-router": "^4.2.4", "vue-router": "^4.2.4",
"vuex": "^4.1.0" "vuex": "^4.1.0"

View File

@ -11,6 +11,8 @@
@mouseup.stop="selectionEnd" @mouseup.stop="selectionEnd"
/> />
<div v-if="!disabled" class="editor-control" :style="editorCtrlStyle"> <div v-if="!disabled" class="editor-control" :style="editorCtrlStyle">
<transition-group appear name="fade-transform" mode="out-in">
<template v-if="!inputing">
<div v-for="(item, index) in iconList" :key="index" class="editor-item"> <div v-for="(item, index) in iconList" :key="index" class="editor-item">
<button <button
class="item-icon" class="item-icon"
@ -88,11 +90,25 @@
</ul> </ul>
</div> </div>
</div> </div>
</template>
<template v-if="inputing">
<div class="editor-item">
<button class="item-icon item-icon-back" @click="backAction" data-info="back">
<svg-icon icon="fe-back" class-name="icon" />
</button>
</div>
<div class="editor-item">
<input type="text" v-model="linkUrl" autofocus @keydown.enter="inputAction" />
<svg-icon icon="msg-enter" class-name="item-enter-icon" />
</div>
</template>
</transition-group>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import SvgIcon from '@/components/SvgIcon.vue' import SvgIcon from '@/components/SvgIcon.vue'
import { websiteValidator } from '@/utils/validator'
export default { export default {
name: 'FreeleapsEditor', name: 'FreeleapsEditor',
props: { props: {
@ -112,6 +128,8 @@ export default {
selectedRange: '', selectedRange: '',
editorCtrlStyle: {}, editorCtrlStyle: {},
commandStates: [], commandStates: [],
inputing: false,
linkUrl: '',
iconList: [ iconList: [
// { // {
// name: 'lable', // hover name // name: 'lable', // hover name
@ -175,6 +193,13 @@ export default {
icon: 'fe-orderedlist', icon: 'fe-orderedlist',
drop: false, drop: false,
canChoose: true canChoose: true
},
{
name: 'link',
type: 'link',
icon: 'fe-link',
drop: false,
canChoose: true
} }
// { // {
// name: 'align-justify', // name: 'align-justify',
@ -202,6 +227,8 @@ export default {
} else { } else {
this.selectedRange = null this.selectedRange = null
this.editorCtrlStyle = { display: 'none' } this.editorCtrlStyle = { display: 'none' }
this.inputing = false
this.linkUrl = ''
// this.commandStates = [] // this.commandStates = []
} }
}, },
@ -225,6 +252,10 @@ export default {
this.$refs.editor.focus() this.$refs.editor.focus()
this.selectedRange = this.getSelect() this.selectedRange = this.getSelect()
this.restoreSelection() this.restoreSelection()
if (type === 'link') {
this.inputing = true
return
}
this.changeStyle(type) this.changeStyle(type)
this.$nextTick(() => { this.$nextTick(() => {
if (dropType) { if (dropType) {
@ -238,6 +269,26 @@ export default {
} }
}) })
}, },
backAction() {
this.inputing = false
this.linkUrl = ''
this.$refs.editor.focus()
this.restoreSelection()
},
inputAction() {
const error = websiteValidator.validate(this.linkUrl)
if (error) {
alert(error)
return
}
const a_node = document.createElement('a')
a_node.setAttribute('href', this.linkUrl)
this.selectedRange.surroundContents(a_node)
this.backAction()
this.$refs.editor.blur()
this.linkUrl = ''
this.editorCtrlStyle = { display: 'none' }
},
getSelect() { getSelect() {
if (window.getSelection) { if (window.getSelection) {
let sel = window.getSelection() let sel = window.getSelection()
@ -417,6 +468,30 @@ export default {
justify-content: center; justify-content: center;
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
input {
height: 24px;
border: 1px solid #e1e1e1;
box-shadow: none;
outline: none;
padding-left: 5px;
padding-right: 24px;
&:focus {
border-color: $primary;
}
}
.item-enter-icon {
position: absolute;
right: 2px;
width: 16px;
height: 16px;
padding: 3px;
background-color: #e1e1e1;
border-radius: 3px;
}
.item-icon { .item-icon {
position: relative; position: relative;
width: 50px; width: 50px;
@ -486,6 +561,9 @@ export default {
&.activity + .dropmenu { &.activity + .dropmenu {
display: block; display: block;
} }
&.item-icon-back {
color: black;
}
} }
} }
</style> </style>
@ -521,4 +599,23 @@ export default {
.editor-body ol { .editor-body ol {
padding-left: 40px; padding-left: 40px;
} }
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 300;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-50px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(50px);
}
.fade-transform-active {
position: absolute;
}
</style> </style>

View File

@ -4,7 +4,7 @@
<div <div
class="information-bar" class="information-bar"
@click="gotoMessages" @click="gotoMessages"
:class="{ active: activePath == 'message', unread: unreadCount > 0 }" :class="{ active: activePath == 'message', unread: unreadConversationCount > 0 }"
> >
<img alt="freeleaps logo" src="@/assets/message.png" /> <img alt="freeleaps logo" src="@/assets/message.png" />
</div> </div>
@ -15,7 +15,7 @@
:class="activePath == 'Workspace' ? 'active' : ''" :class="activePath == 'Workspace' ? 'active' : ''"
> >
<svg-icon icon="workspace" class-name="icon" /> <svg-icon icon="workspace" class-name="icon" />
Workspace {{ $t('Workspace') }}
</button> </button>
<button <button
class="navigation-item" class="navigation-item"
@ -23,7 +23,7 @@
:class="activePath == 'Requests' ? 'active' : ''" :class="activePath == 'Requests' ? 'active' : ''"
> >
<svg-icon icon="requests" class-name="icon" /> <svg-icon icon="requests" class-name="icon" />
Requests {{ $t('Requests') }}
</button> </button>
<button <button
class="navigation-item" class="navigation-item"
@ -31,7 +31,7 @@
:class="activePath == 'Providers' ? 'active' : ''" :class="activePath == 'Providers' ? 'active' : ''"
> >
<svg-icon icon="providers" class-name="icon" /> <svg-icon icon="providers" class-name="icon" />
Providers {{ $t('Providers') }}
</button> </button>
<button <button
class="navigation-item" class="navigation-item"
@ -39,7 +39,7 @@
:class="activePath == 'Post' ? 'active' : ''" :class="activePath == 'Post' ? 'active' : ''"
> >
<svg-icon icon="post" class-name="icon" /> <svg-icon icon="post" class-name="icon" />
Post {{ $t('Post') }}
</button> </button>
<div class="form-check form-switch header-switch-container"> <div class="form-check form-switch header-switch-container">
<input <input
@ -50,10 +50,10 @@
disabled disabled
/> />
<label class="form-check-label" for="personal-earning-now-checkbox"> <label class="form-check-label" for="personal-earning-now-checkbox">
<span>Providing service</span> <span>{{ $t('Providing service') }}</span>
</label> </label>
<div class="header-switch-desc"> <div class="header-switch-desc">
Please go to profile page to add money receiving method {{ $t('Please go to profile page to add money receiving method') }}
</div> </div>
</div> </div>
<laguage-switch class="laguage-switch" /> <laguage-switch class="laguage-switch" />
@ -68,17 +68,17 @@
/> />
<ul class="dropdown-menu" aria-labelledby="accountButton"> <ul class="dropdown-menu" aria-labelledby="accountButton">
<li> <li>
<button class="account-menu-button" @click="gotoProfile">Profile</button> <button class="account-menu-button" @click="gotoProfile">{{ $t('Profile') }}</button>
</li> </li>
<li> <li>
<button class="account-menu-button" @click="gotoHistory">History</button> <button class="account-menu-button" @click="gotoHistory">{{ $t('History') }}</button>
</li> </li>
<li> <li>
<hr class="dropdown-divider" /> <hr class="dropdown-divider" />
</li> </li>
<li> <li>
<button class="account-menu-button" @click="signout"> <button class="account-menu-button" @click="signout">
Log out ({{ userIdentityNote }}) {{ $t('Log out') }} ({{ userIdentityNote }})
</button> </button>
</li> </li>
</ul> </ul>
@ -94,8 +94,8 @@ export default {
name: 'HeaderGuest', name: 'HeaderGuest',
components: { LaguageSwitch }, components: { LaguageSwitch },
computed: { computed: {
unreadCount() { unreadConversationCount() {
return this.$store.getters['basic/unreadCount'] return this.$store.getters['basic/unreadConversationCount']
} }
}, },
created() { created() {

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16315" width="64" height="64"><path d="M189.056 547.072l554.666667 384A42.666667 42.666667 0 0 0 810.666667 896V128a42.666667 42.666667 0 0 0-66.944-35.114667l-554.666667 384a42.794667 42.794667 0 0 0 0 70.186667z" p-id="16316"></path></svg>

After

Width:  |  Height:  |  Size: 325 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4251" width="64" height="64"><path d="M607.934444 417.856853c-6.179746-6.1777-12.766768-11.746532-19.554358-16.910135l-0.01228 0.011256c-6.986111-6.719028-16.47216-10.857279-26.930349-10.857279-21.464871 0-38.864146 17.400299-38.864146 38.864146 0 9.497305 3.411703 18.196431 9.071609 24.947182l-0.001023 0c0.001023 0.001023 0.00307 0.00307 0.005117 0.004093 2.718925 3.242857 5.953595 6.03853 9.585309 8.251941 3.664459 3.021823 7.261381 5.997598 10.624988 9.361205l3.203972 3.204995c40.279379 40.229237 28.254507 109.539812-12.024871 149.820214L371.157763 796.383956c-40.278355 40.229237-105.761766 40.229237-146.042167 0l-3.229554-3.231601c-40.281425-40.278355-40.281425-105.809861 0-145.991002l75.93546-75.909877c9.742898-7.733125 15.997346-19.668968 15.997346-33.072233 0-23.312962-18.898419-42.211381-42.211381-42.211381-8.797363 0-16.963347 2.693342-23.725354 7.297197-0.021489-0.045025-0.044002-0.088004-0.066515-0.134053l-0.809435 0.757247c-2.989077 2.148943-5.691629 4.669346-8.025791 7.510044l-78.913281 73.841775c-74.178443 74.229608-74.178443 195.632609 0 269.758863l3.203972 3.202948c74.178443 74.127278 195.529255 74.127278 269.707698 0l171.829484-171.880649c74.076112-74.17435 80.357166-191.184297 6.282077-265.311575L607.934444 417.856853z" fill="#5D5D5D" p-id="4252"></path><path d="M855.61957 165.804257l-3.203972-3.203972c-74.17742-74.178443-195.528232-74.178443-269.706675 0L410.87944 334.479911c-74.178443 74.178443-78.263481 181.296089-4.085038 255.522628l3.152806 3.104711c3.368724 3.367701 6.865361 6.54302 10.434653 9.588379 2.583848 2.885723 5.618974 5.355985 8.992815 7.309476 0.025583 0.020466 0.052189 0.041956 0.077771 0.062422l0.011256-0.010233c5.377474 3.092431 11.608386 4.870938 18.257829 4.870938 20.263509 0 36.68962-16.428158 36.68962-36.68962 0-5.719258-1.309832-11.132548-3.645017-15.95846l0 0c-4.850471-10.891048-13.930267-17.521049-20.210297-23.802102l-3.15383-3.102664c-40.278355-40.278355-24.982998-98.79612 15.295358-139.074476l171.930791-171.830507c40.179095-40.280402 105.685018-40.280402 145.965419 0l3.206018 3.152806c40.279379 40.281425 40.279379 105.838513 0 146.06775l-75.686796 75.737962c-10.296507 7.628748-16.97358 19.865443-16.97358 33.662681 0 23.12365 18.745946 41.87062 41.87062 41.87062 8.048303 0 15.563464-2.275833 21.944801-6.211469 0.048095 0.081864 0.093121 0.157589 0.141216 0.240477l1.173732-1.083681c3.616364-2.421142 6.828522-5.393847 9.529027-8.792247l79.766718-73.603345C929.798013 361.334535 929.798013 239.981676 855.61957 165.804257z"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -180,5 +180,28 @@ export default {
'Payment plan proposed by the service provider': 'Payment plan proposed by the service provider', 'Payment plan proposed by the service provider': 'Payment plan proposed by the service provider',
'Execution plan proposed by the service provider': 'Execution plan proposed by the service provider':
'Execution plan proposed by the service provider', 'Execution plan proposed by the service provider',
'Proceed to workspace': 'Proceed to workspace' 'Proceed to workspace': 'Proceed to workspace',
Workspace: 'Workspace',
Requests: 'Requests',
Providers: 'Providers',
Post: 'Post',
'Providing service': 'Providing service',
'Please go to profile page to add money receiving method':
'Please go to profile page to add money receiving method',
History: 'History',
'Log out': 'Log out',
Proposals: 'Proposals',
'Milestone(s)': 'Milestone(s)',
Information: 'Information',
'Operation Complete': 'Operation Complete',
'Wait for completion': 'Wait for completion',
'Mission complete': 'Mission complete',
'Wait for payment': 'Wait for payment',
'Wait for confirmation': 'Wait for confirmation',
'Confirm receipt': 'Confirm receipt',
Ready: 'Ready',
'Not available': 'Not available',
'Please input issue description': 'Please input issue description',
'day(s)': 'day(s)'
} }

View File

@ -13,6 +13,7 @@
</div> </div>
</template> </template>
<script> <script>
import cover_picture from '@/assets/images/lab-translation.png'
export default { export default {
name: 'LabHome', name: 'LabHome',
components: {}, components: {},
@ -31,7 +32,28 @@ export default {
title_text: 'Machine Translation', title_text: 'Machine Translation',
summary_text: 'Translate lanuages leverage AI power', summary_text: 'Translate lanuages leverage AI power',
icon_picture: '', icon_picture: '',
cover_picture: 'src/assets/images/lab-translation.png' cover_picture: cover_picture
},
{
path: 'task-completion',
title_text: 'Task Completion',
summary_text: 'Respone for a user prompty',
icon_picture: '',
cover_picture: cover_picture
},
{
path: 'multiturn-chat',
title_text: 'Multi turn chat',
summary_text: 'Respone based on multi turn messages ',
icon_picture: '',
cover_picture: cover_picture
},
{
path: 'image-generation',
title_text: 'image generation',
summary_text: 'Generate a image based on user prompt',
icon_picture: '',
cover_picture: cover_picture
} }
] ]
} }

View File

@ -0,0 +1,52 @@
<template>
<div class="input_containter">
<input
class="input_text"
type="text"
v-model="input_text"
@keyup.enter="image_generation($event)"
/>
<img class="responded_image" :src="responded_image" />
</div>
</template>
<script>
import { LabApi } from '@/utils/index'
export default {
name: 'ImageGeneration',
components: {},
computed: {},
mounted() {},
methods: {
image_generation($event) {
LabApi.image_generation(this.input_text)
.then((response) => {
this.responded_image = response.data
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
}
},
data() {
return {
input_text: null,
responded_image: null
}
}
}
</script>
<style scoped lang="scss">
.input_containter {
@extend .container;
@extend .m-3;
}
.input_text {
@extend .w-100;
}
.responded_image {
@extend .w-100;
}
</style>

View File

@ -0,0 +1,56 @@
<template>
<div class="input_containter">
<input class="input_text" type="text" v-model="input_text" @keyup.enter="lets_chat($event)" />
<p class="responded_text">{{ responded_text }}</p>
</div>
</template>
<script>
import { LabApi } from '@/utils/index'
export default {
name: 'MultiturnChat',
components: {},
computed: {},
mounted() {},
methods: {
lets_chat($event) {
this.messages.push({
role: 'user',
content: this.input_text
})
LabApi.multiturn_chat(this.messages)
.then((response) => {
this.responded_text = response.data
this.messages.push({
role: 'assistant',
content: this.responded_text
})
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
}
},
data() {
return {
input_text: null,
messages: [{ role: 'system', content: 'You are a helpful assistant.' }],
responded_text: null
}
}
}
</script>
<style scoped lang="scss">
.input_containter {
@extend .container;
@extend .m-3;
}
.input_text {
@extend .w-100;
}
.responded_text {
@extend .w-100;
}
</style>

View File

@ -0,0 +1,52 @@
<template>
<div class="input_containter">
<input
class="input_text"
type="text"
v-model="input_text"
@keyup.enter="task_completion($event)"
/>
<p class="responded_text">{{ responded_text }}</p>
</div>
</template>
<script>
import { LabApi } from '@/utils/index'
export default {
name: 'TaskCompletion',
components: {},
computed: {},
mounted() {},
methods: {
task_completion($event) {
LabApi.task_completion(this.input_text)
.then((response) => {
this.responded_text = response.data
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
}
},
data() {
return {
input_text: null,
responded_text: null
}
}
}
</script>
<style scoped lang="scss">
.input_containter {
@extend .container;
@extend .m-3;
}
.input_text {
@extend .w-100;
}
.responded_text {
@extend .w-100;
}
</style>

View File

@ -1,7 +1,11 @@
<template> <template>
<div class="directories_containter"> <div class="directories_containter">
<div class="directory_container" v-for="(directory, index) in directories" :key="index" <div
@click="view_link(directory)"> class="directory_container"
v-for="(directory, index) in directories"
:key="index"
@click="view_link(directory)"
>
<img class="directory_cover_image" :src="directory.cover_picture" /> <img class="directory_cover_image" :src="directory.cover_picture" />
<p class="directory-title">{{ directory.title_text }}</p> <p class="directory-title">{{ directory.title_text }}</p>
<p class="directory-subtitle">{{ directory.summary_text }}</p> <p class="directory-subtitle">{{ directory.summary_text }}</p>

View File

@ -1,6 +1,11 @@
<template> <template>
<div v-if="blogs" class="blogs_containter"> <div v-if="blogs" class="blogs_containter">
<div class="blog_containter" v-for="(blog, index) in blogs" :key="index" @click="view_blog(blog)"> <div
class="blog_containter"
v-for="(blog, index) in blogs"
:key="index"
@click="view_blog(blog)"
>
<h2>{{ blog.blog_name }}</h2> <h2>{{ blog.blog_name }}</h2>
<img class="blog_cover_image" :src="blog.cover_picture" /> <img class="blog_cover_image" :src="blog.cover_picture" />
<p v-text="retrieve_summary(blog)"></p> <p v-text="retrieve_summary(blog)"></p>

View File

@ -1,6 +1,11 @@
<template> <template>
<div class="career_containter"> <div class="career_containter">
<div class="career-item" v-for="(directory, index) in directories" :key="index" @click="view_link(directory)"> <div
class="career-item"
v-for="(directory, index) in directories"
:key="index"
@click="view_link(directory)"
>
<p class="career-title"> <p class="career-title">
{{ directory.title_text }} {{ directory.title_text }}
</p> </p>

View File

@ -271,7 +271,11 @@
class="profile-photo" class="profile-photo"
alt="user portrait" alt="user portrait"
id="personal-photo-operation-image" id="personal-photo-operation-image"
src="@/assets/profile.png" :src="
userProfile.account.basic.photo.base64
? userProfile.account.basic.photo.base64
: profileUrl
"
/> />
<label class="profile-item-label" for="personal-photo-operation-image">{{ <label class="profile-item-label" for="personal-photo-operation-image">{{
$t('Portrait') $t('Portrait')
@ -362,7 +366,11 @@
class="user-portrait-img" class="user-portrait-img"
id="personal-photo-operation-image" id="personal-photo-operation-image"
alt="user portrait" alt="user portrait"
src="@/assets/profile.png" :src="
userProfile.account.basic.photo.base64
? userProfile.account.basic.photo.base64
: profileUrl
"
v-tooltip v-tooltip
title="Click to update" title="Click to update"
@click="selectUserPhoto()" @click="selectUserPhoto()"
@ -521,7 +529,7 @@
<div class="panel-table-content"> <div class="panel-table-content">
<span class="panel-table-label">{{ $t('On Freeleaps') }}</span> <span class="panel-table-label">{{ $t('On Freeleaps') }}</span>
<span class="panel-table-span"> <span class="panel-table-span">
{{ userProfile.achievemnt.activeness.days_of_staying_on }} day(s) {{ userProfile.achievemnt.activeness.days_of_staying_on }} {{ $t('day(s)') }}
</span> </span>
</div> </div>
</td> </td>
@ -724,7 +732,7 @@
<script> <script>
import SvgIcon from '@/components/SvgIcon.vue' import SvgIcon from '@/components/SvgIcon.vue'
import { moneyCollectionTypeEnum } from '@/types/index' import { moneyCollectionTypeEnum } from '@/types/index'
import profileUrl from '@/assets/profile.png'
import { UserProfileApi, elementHandler, textAreaAujuster, passwordValidator } from '@/utils/index' import { UserProfileApi, elementHandler, textAreaAujuster, passwordValidator } from '@/utils/index'
import FreeleapsEditor from '@/components/FreeleapsEditor.vue' import FreeleapsEditor from '@/components/FreeleapsEditor.vue'
@ -764,6 +772,7 @@ export default {
}, },
data() { data() {
return { return {
profileUrl,
userProfile: null, userProfile: null,
message: null, message: null,
accountNeedAttention: false, accountNeedAttention: false,
@ -886,7 +895,7 @@ export default {
updatePhoto(base64, filename) { updatePhoto(base64, filename) {
UserProfileApi.updateUserPhoto(base64, filename) UserProfileApi.updateUserPhoto(base64, filename)
.then((response) => { .then((response) => {
this.userProfile.account.basic.photo = response.data this.userProfile.account.basic.photo = response.data.photo
this.updateLocalIdentityData() this.updateLocalIdentityData()
}) })
.catch((error) => { .catch((error) => {

View File

@ -7,7 +7,7 @@
:key="index" :key="index"
class="conversation-container" class="conversation-container"
:class="{ :class="{
selected: current_thread?.conversation?.id === conversation.id selected: selConversation?.id === conversation?.id
}" }"
@click="selectConversation(conversation)" @click="selectConversation(conversation)"
> >
@ -20,11 +20,10 @@
<span class="conversation-last-update-date">{{ <span class="conversation-last-update-date">{{
getDateFromFulltimeString(conversation.create_time) getDateFromFulltimeString(conversation.create_time)
}}</span> }}</span>
<!-- <span v-if="unreadCountMapper()" class="conversation-unreadcount">2</span> -->
</div> </div>
<!-- <div class="conversation-summary-highlight-container"> <div class="conversation-summary-highlight-container">
{{ conversation.summary.last_message?.message_body }} {{ conversation.last_message?.message_body }}
</div> --> </div>
</div> </div>
</div> </div>
</div> </div>
@ -35,12 +34,12 @@
{{ $t('Empty conversation') }} {{ $t('Empty conversation') }}
</div> </div>
<div class="message-panel-container"> <div class="message-panel-container">
<div v-if="current_thread" class="message-thread-container"> <div v-if="messages && messages.length > 0" class="message-thread-container">
<div <div
v-for="(item, index) in current_thread.conversation.messages" v-for="(item, index) in messages"
:key="index" :key="index"
class="message-item-container" class="message-item-container"
:class="item.raw_data.sender_id == userIdentityNote ? 'me' : ''" :class="item.sender_id == userIdentityNote ? 'me' : ''"
> >
<div class="message-item-header-container"> <div class="message-item-header-container">
<img <img
@ -49,17 +48,17 @@
src="@/assets/profile.png" src="@/assets/profile.png"
/> />
<span class="message-item-sender-fullname"> <span class="message-item-sender-fullname">
{{ item.sender_profile.first_name }} {{ item.sender_firstname }}
{{ item.sender_profile.last_name }} {{ item.sender_lastname }}
</span> </span>
<span class="message-item-create-time"> <span class="message-item-create-time">
{{ getDateFromFulltimeString(item.raw_data.create_time) }}</span {{ getDateFromFulltimeString(item.create_time) }}</span
> >
</div> </div>
<div class="message-item-message-body">{{ item.raw_data.message_body }}</div> <div class="message-item-message-body">{{ item.message_body }}</div>
</div> </div>
</div> </div>
<div v-if="!current_thread" class="message-thread-empty-container"> <div v-if="!messages || messages.length == 0" class="message-thread-empty-container">
{{ $t('Please choose conversation') }} {{ $t('Please choose conversation') }}
</div> </div>
<div class="message-writing-panel-container"> <div class="message-writing-panel-container">
@ -68,7 +67,7 @@
class="writing-message-input" class="writing-message-input"
type="text" type="text"
v-model="writtenMessage" v-model="writtenMessage"
@keypress.enter="sendMessage(current_thread.conversation.information.conversation_id)" @keypress.enter="sendMessage(selConversation.id)"
/> />
</div> </div>
</div> </div>
@ -78,58 +77,49 @@
<script> <script>
import SvgIcon from '@/components/SvgIcon.vue' import SvgIcon from '@/components/SvgIcon.vue'
import { MessageHubApi, DateUtils } from '@/utils/index' import { MessageHubApi, DateUtils } from '@/utils/index'
import { userUtils } from '@/utils/store/index'
export default { export default {
components: { SvgIcon }, components: { SvgIcon },
name: 'MessageHub', name: 'MessageHub',
props: {}, props: {},
mounted() { mounted() {},
this.fetchConversations()
},
watch: {
conversations: {
handler: function (val) {
if (val && val.length > 0) {
//this.current_thread = val[0]
//this.clearUnreadMessageBy(val[0])
}
},
deep: false
}
},
data() { data() {
return { return {
userIdentityNote: this.mnx_getUserIdentity(), userIdentityNote: this.mnx_getUserIdentity(),
conversations: [], selConversation: null,
current_thread: null, messages: [],
writtenMessage: null writtenMessage: null
} }
}, },
// computed: { computed: {
// unreadCountMapper() { conversations() {
// return this.$store.getters['basic/unreadCountMapper'] return this.$store.getters['basic/conversations']
// } }
// },
methods: {
fetchConversations() {
MessageHubApi.fetchConversations(new Date('01 Jan 1970 00:00:00 GMT').toISOString())
.then((response) => {
this.conversations = response.data.conversations
//TEST
this.conversations.forEach((conversation) => {
this.fetchMessageForConversation(conversation.id)
})
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
})
}, },
watch: {
conversations(n_val) {
if (!this.selConversation && n_val[0]) {
this.selConversation = n_val[0]
this.messages = n_val[0].messages || []
this.clearUnreadMessageBy(n_val[0])
} else {
if (this.selConversation.id === n_val?.[0]?.id) {
this.messages = n_val[0].messages || []
this.clearUnreadMessageBy(n_val[0])
}
}
}
},
methods: {
fetchMessageForConversation(conversation_id) { fetchMessageForConversation(conversation_id) {
const jwt = userUtils.getJwtToken()
MessageHubApi.fetchMessages( MessageHubApi.fetchMessages(
conversation_id, conversation_id,
new Date('01 Jan 1975 00:00:00 GMT').toISOString() new Date('01 Jan 1975 00:00:00 GMT').toISOString(),
jwt
) )
.then((response) => { .then((response) => {
messages = response.data this.messages = response?.data || []
console.log('Received message for conversation:', conversation_id) console.log('Received message for conversation:', conversation_id)
}) })
.catch((error) => { .catch((error) => {
@ -137,20 +127,26 @@ export default {
}) })
}, },
selectConversation(conversation) { selectConversation(conversation) {
this.current_thread = conversation this.selConversation = conversation
this.fetchMessageForConversation(conversation.id)
this.clearUnreadMessageBy(conversation) this.clearUnreadMessageBy(conversation)
}, },
clearUnreadMessageBy(current) { clearUnreadMessageBy(current) {
const sender = current.conversation.messages?.[0].raw_data.sender_id if (current.unread) {
if (sender) { this.$store.dispatch('basic/readMessageBy', current.id)
this.$store.dispatch('basic/readMessageBy', sender)
} }
}, },
sendMessage(conversation_id) { sendMessage(conversation_id) {
MessageHubApi.sendMessageToConversation(conversation_id, this.writtenMessage) const jwt = userUtils.getJwtToken()
MessageHubApi.sendMessageToConversation(conversation_id, this.writtenMessage, jwt)
.then((response) => { .then((response) => {
let new_message = response.data let new_message = response.data
this.current_thread.conversation.messages.push(new_message) this.messages.push({
...new_message.raw_data,
sender_firstname: new_message.sender_profile.first_name,
sender_lastname: new_message.sender_profile.last_name,
sender_photo: new_message.sender_profile.photo
})
this.writtenMessage = null this.writtenMessage = null
}) })
.catch((error) => { .catch((error) => {
@ -218,6 +214,7 @@ export default {
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
text-align: left;
} }
.conversation-last-update-date { .conversation-last-update-date {

View File

@ -34,7 +34,8 @@
>{{ $t('Stay on Freeleaps') }}</label >{{ $t('Stay on Freeleaps') }}</label
> >
<span class="provider-stay-on-freeleaps-span" id="provider-stay-on-freeleaps"> <span class="provider-stay-on-freeleaps-span" id="provider-stay-on-freeleaps">
{{ provider.activeness_achievement.days_of_staying_on }} day(s)</span {{ provider.activeness_achievement.days_of_staying_on }}
{{ $t('day(s)') }}</span
> >
</div> </div>
<div class="provider-delivered-projects-container"> <div class="provider-delivered-projects-container">
@ -101,7 +102,8 @@
$t('Project delivering time') $t('Project delivering time')
}}</label> }}</label>
<span class="dd-project-span" id="delivery-time-per-project"> <span class="dd-project-span" id="delivery-time-per-project">
{{ provider.provider_deliveries.delivering_time_per_project_in_day }} day(s) {{ provider.provider_deliveries.delivering_time_per_project_in_day }}
{{ $t('day(s)') }}
</span> </span>
</div> </div>
<div class="dd-project-container"> <div class="dd-project-container">
@ -445,11 +447,6 @@ export default {
border-top: 1px solid #dee2e6; border-top: 1px solid #dee2e6;
} }
// .statistics-content-label {
// @extend .label-text-light;
// @extend .w-100;
// }
.statistics-content-container { .statistics-content-container {
@extend .flex-colum-container; @extend .flex-colum-container;
padding: 0; padding: 0;

View File

@ -77,11 +77,23 @@
{{ $t('Propose') }} {{ $t('Propose') }}
</button> </button>
<div class="request-description-content" v-html="request.content"></div> <div class="request-description-content" v-html="request.content"></div>
<div v-for="(file, index) in request.attached_files" :key="index"> <div
<button @click="previewAttachedFile(request.id, file.document_id)"> class="pdf-actions"
v-for="(file, index) in request.attached_files"
:key="index"
>
<button
class="btn btn-link"
data-bs-toggle="modal"
data-bs-target="#pdf-viewer"
@click="previewAttachedFile(request.id, file.document_id, file.file_name)"
>
{{ $t('Preview') }}{{ file.file_name }} {{ $t('Preview') }}{{ file.file_name }}
</button> </button>
<button @click="downloadAttachedFile(request.id, file.document_id)"> <button
class="btn btn-link"
@click="downloadAttachedFile(request.id, file.document_id)"
>
{{ $t('Download') }}{{ file.file_name }} {{ $t('Download') }}{{ file.file_name }}
</button> </button>
</div> </div>
@ -119,7 +131,7 @@
> >
<span class="issuer-achievement-stay-content-text"> <span class="issuer-achievement-stay-content-text">
{{ request.issuer_achievement.activeness.days_of_staying_on }} {{ request.issuer_achievement.activeness.days_of_staying_on }}
day(s)</span {{ $t('day(s)') }}</span
> >
</div> </div>
</div> </div>
@ -179,12 +191,38 @@
</div> </div>
</div> </div>
</div> </div>
<div
class="modal fade"
id="pdf-viewer"
tabindex="-1"
aria-labelledby="pdf-viewer-label"
aria-hidden="true"
>
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="pdf-viewer-label">{{ pdfDocument.title }}</h1>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<PDFReader :doc="pdfDocument.doc" />
</div>
</div>
</div>
</div>
</template> </template>
<script> <script>
import { RequestHubApi, WorksapceApi, DateUtils, requestHubUtils } from '@/utils/index' import { RequestHubApi, WorksapceApi, DateUtils, requestHubUtils } from '@/utils/index'
import { proposingModelEnum } from '@/types/index' import { proposingModelEnum } from '@/types/index'
import PDFReader from '@/components/PDFReader.vue'
export default { export default {
components: { PDFReader },
name: 'RequestHub', name: 'RequestHub',
props: {}, props: {},
mounted() { mounted() {
@ -193,7 +231,8 @@ export default {
data() { data() {
return { return {
requestGroups: [], requestGroups: [],
message: null message: null,
pdfDocument: {}
} }
}, },
@ -214,17 +253,16 @@ export default {
getDateFromFulltimeString(fulltime) { getDateFromFulltimeString(fulltime) {
return DateUtils.FromJsonToDateString(fulltime) return DateUtils.FromJsonToDateString(fulltime)
}, },
previewAttachedFile(request_id, document_id) { previewAttachedFile(request_id, document_id, title) {
// !!! SHOULD NOT use PdfContentViewer which is designed for unlogged in users. this.pdfDocument.title = title
// !!! Instead, should have a dedicated pdf viewer which should follow the figma design. WorksapceApi.fetchAttachedFileAsMediaData(request_id, document_id)
// WorksapceApi.fetchAttachedFileAsMediaData(request_id, document_id) .then((response) => {
// .then((response) => { let media_data = response.data
// let media_data = response.data this.pdfDocument.doc = { url: media_data }
// console.log(media_data) })
// }) .catch((error) => {
// .catch((error) => { this.mnx_backendErrorHandler(error)
// this.mnx_backendErrorHandler(error) })
// })
}, },
downloadAttachedFile(request_id, document_id) { downloadAttachedFile(request_id, document_id) {
WorksapceApi.fetchAttachedFileAsDownload(request_id, document_id) WorksapceApi.fetchAttachedFileAsDownload(request_id, document_id)
@ -413,4 +451,16 @@ export default {
@extend .text-start; @extend .text-start;
font-weight: bold; font-weight: bold;
} }
.pdf-actions {
text-align: left;
display: flex;
align-items: center;
margin-top: 12px;
.btn-link {
padding: 0;
margin-right: 12px;
}
}
</style> </style>

View File

@ -12,10 +12,6 @@
</template> </template>
<script> <script>
import {
UserProfileApi
// userProfileValidator,
} from '@/utils/index'
export default { export default {
name: 'ProposalSubmitted', name: 'ProposalSubmitted',
props: { props: {

View File

@ -199,7 +199,7 @@
/> />
<label :for="`stage-duration-content-${index}`">{{ $t('Duration') }}</label> <label :for="`stage-duration-content-${index}`">{{ $t('Duration') }}</label>
</div> </div>
<span class="btn-start">day(s)</span> <span class="btn-start">{{ $t('day(s)') }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -221,36 +221,6 @@
<button class="stage-item-delete-button"> <button class="stage-item-delete-button">
<svg-icon v-if="index != 0" icon="delete" @click="removeStage(index)" /> <svg-icon v-if="index != 0" icon="delete" @click="removeStage(index)" />
</button> </button>
<!-- <div class="stage-item-content-container" id="stage-item-content">
<div class="stage-payment-container">
<label class="stage-content-label" for="stage-payment-content">Payment</label>
<div class="stage-payment-content-container" id="stage-payment-content">
<input
type="text"
class="stage-payment-content-text"
id="stage-payment-content-text"
v-model="stage.payment"
/>
<span class="stage-payment-content-span"> {{ stage.currency }}</span>
</div>
</div>
<div class="stage-duration-container">
<label class="stage-content-label" for="stage-duration-content">Duration</label>
<div class="stage-duration-content-container" id="stage-duration-content">
<input
type="text"
class="stage-duration-content-text"
v-model="stage.duration_in_days"
/>
<span class="stage-duration-content-span"> day(s)</span>
</div>
</div>
<div class="stage-note-container">
<label class="stage-content-label" for="stage-note-content">Notes</label>
<input class="stage-note-content-text" id="stage-note-content" v-model="stage.note" />
</div>
</div> -->
</div> </div>
<div class="stage-more-action-container"> <div class="stage-more-action-container">
<button class="stage-add-more-button" @click="addStage()"> <button class="stage-add-more-button" @click="addStage()">
@ -266,7 +236,7 @@
{{ $t('Total payment') }}:{{ summary.total_payment }} {{ summary.currency }} {{ $t('Total payment') }}:{{ summary.total_payment }} {{ summary.currency }}
</span> </span>
<span id="summary-total-duration-content"> <span id="summary-total-duration-content">
{{ $t('Total duration') }}:{{ summary.total_duration_in_days }} day(s) {{ $t('Total duration') }}:{{ summary.total_duration_in_days }} {{ $t('day(s)') }}
</span> </span>
</div> </div>
</div> </div>
@ -350,12 +320,6 @@ export default {
} }
}, },
methods: { methods: {
// loadTemplates() {
// this.mnx_navToLoadTemplates()
// },
// copyExisting() {
// this.mnx_navToCopyProposals()
// },
isUserInCNY() { isUserInCNY() {
return window.location.host.includes('localhost') || window.location.href.includes('com.cn') return window.location.host.includes('localhost') || window.location.href.includes('com.cn')
}, },

View File

@ -207,7 +207,7 @@
></div> ></div>
</div> </div>
</div> </div>
<div class="accordion-body" v-if="isOngoingProject(project)"> <div class="accordion-body inline-accordion-body" v-if="isOngoingProject(project)">
<div class="project-invite-collaborator-containter"> <div class="project-invite-collaborator-containter">
<button <button
class="accordion-button collapsed" class="accordion-button collapsed"
@ -225,23 +225,15 @@
data-bs-parent="#collapse-project-invite-collaborator" data-bs-parent="#collapse-project-invite-collaborator"
> >
<div class="project-invite-collaborator-form-container"> <div class="project-invite-collaborator-form-container">
<div class="project-invite-collaborator-form">
<label class="project-item-label">{{
$t('Input E-mail to invite other')
}}</label>
<input <input
class="project-invite-collaborator-input" type="text"
v-model="newInviteCollaborator[project_index]" v-model="newInviteCollaborator[project_index]"
/> :placeholder="$t('Input E-mail to invite other')"
</div> @keydown.enter="
<button
class="project-invite-collaborator-action-button"
@click="
inviteCollaborator(project.project_id, newInviteCollaborator[project_index]) inviteCollaborator(project.project_id, newInviteCollaborator[project_index])
" "
> />
{{ $t('Submit') }} <svg-icon icon="msg-enter" class-name="project-invite-enter" />
</button>
</div> </div>
</div> </div>
</div> </div>
@ -256,7 +248,7 @@
aria-expanded="false" aria-expanded="false"
aria-controls="collapse-project-milestone" aria-controls="collapse-project-milestone"
> >
<div class="project-milestone-bar-container"> <div class="project-milestone-bar-container dashed-container">
<div class="project-milestone-bar-progress"> <div class="project-milestone-bar-progress">
<label class="project-item-label">{{ $t('Progress') }}</label> <label class="project-item-label">{{ $t('Progress') }}</label>
<p class="project-item-text"> <p class="project-item-text">
@ -296,65 +288,92 @@
class="accordion-collapse collapse" class="accordion-collapse collapse"
data-bs-parent="#collapse-project-milestone" data-bs-parent="#collapse-project-milestone"
> >
<div class="project-milestones-containter"> <table class="project-milestones-table">
<div <tbody
class="project-milestone-container"
v-for="milestone in project.project.progress.milestones" v-for="milestone in project.project.progress.milestones"
:key="milestone.index" :key="milestone.index"
:id="'project-milestone-' + milestone.index" :id="'project-milestone-' + milestone.index"
> >
<div class="project-milestone-index"> <tr>
<label class="project-item-label">{{ $t('Milestone') }}</label> <td>
<p class="project-item-text"> <div class="project-milestones-table-content">
{{ milestone.index }} <span class="project-milestones-table-label">{{
</p> $t('Milestone')
}}</span>
<span class="project-milestones-table-span">{{
milestone.index
}}</span>
</div> </div>
<div class="project-milestone-description"> </td>
<label class="project-item-label">{{ $t('Description') }}</label> <td>
<p class="project-item-text"> <div class="project-milestones-table-content">
{{ milestone.description }} <span class="project-milestones-table-label">{{
</p> $t('Description')
}}</span>
<span class="project-milestones-table-span">{{
milestone.description
}}</span>
</div> </div>
<div class="project-milestone-status"> </td>
<label class="project-item-label">{{ $t('Status') }}</label> <td>
<p class="project-item-text"> <div class="project-milestones-table-content">
{{ fromIntToMilestoneStatus(milestone.status) }} <span class="project-milestones-table-label">{{ $t('Status') }}</span>
</p> <span class="project-milestones-table-span">{{
fromIntToMilestoneStatus(milestone.status)
}}</span>
</div> </div>
<div class="project-milestone-payment"> </td>
<label class="project-item-label">{{ $t('Payment') }}</label> <td>
<p class="project-item-text"> <div class="project-milestones-table-content">
<span class="project-milestones-table-label">{{
$t('Payment')
}}</span>
<span class="project-milestones-table-span">
{{ milestone.actual_paid }} / {{ milestone.expected_payment }} {{ milestone.actual_paid }} / {{ milestone.expected_payment }}
{{ project.project.progress.payment_currency }} {{ project.project.progress.payment_currency }}
</p> </span>
</div> </div>
<div class="project-milestone-update"> </td>
<label class="project-item-label">{{ $t('Update') }}</label> <td>
<p class="project-item-text"> <div class="project-milestones-table-content">
{{ getDateFromFulltimeString(milestone.update_time) }} <span class="project-milestones-table-label">{{ $t('Update') }}</span>
</p> <span class="project-milestones-table-span">{{
getDateFromFulltimeString(milestone.update_time)
}}</span>
</div> </div>
<div class="project-milestone-action-button-container"> </td>
<td>
<div class="project-milestones-table-content">
<span class="project-milestones-table-label">{{ $t('Action') }}</span>
<button <button
class="project-milestone-action-button" class="btn btn-link"
:disabled="isMilestoneActionButtonDisabled(project.project, milestone)" :disabled="
isMilestoneActionButtonDisabled(project.project, milestone)
"
:hidden="isMilestoneActionButtonHidden(project.project, milestone)" :hidden="isMilestoneActionButtonHidden(project.project, milestone)"
@click="handleMilestoneAction(project.project, milestone)" @click="handleMilestoneAction(project.project, milestone)"
> >
{{ fetchMilestoneActionButtonText(project.project, milestone) }} {{ fetchMilestoneActionButtonText(project.project, milestone) }}
</button> </button>
<!-- <span class="project-milestones-table-span">{{ getDateFromFulltimeString(milestone.update_time) }}</span> -->
</div> </div>
<div v-if="this.qrCode.index === milestone.index"> </td>
<img width="100" height="100" :src="this.qrCode.imageUrl" /> </tr>
<tr v-if="this.qrCode.index === milestone.index">
<td colspan="6">
<div class="project-milestones-qrcode-content">
<img :src="this.qrCode.imageUrl" alt="freeleaps" />
<button <button
class="project-milestone-payment-confirm-button" class="project-milestones-qrcode-button"
@click="handlePaymentAction(project.project, milestone)" @click="handlePaymentAction(project.project, milestone)"
> >
{{ $t('Mark As Paid') }} {{ $t('Mark As Paid') }}
</button> </button>
</div> </div>
</div> </td>
</div> </tr>
</tbody>
</table>
</div> </div>
</div> </div>
<div class="accordion-item"> <div class="accordion-item">
@ -367,7 +386,7 @@
aria-expanded="false" aria-expanded="false"
aria-controls="collapse-project-code" aria-controls="collapse-project-code"
> >
<div class="project-code-bar-container"> <div class="project-code-bar-container dashed-container">
<div class="project-code-git-status"> <div class="project-code-git-status">
<label class="project-item-label">{{ $t('Code Depot') }}</label> <label class="project-item-label">{{ $t('Code Depot') }}</label>
<p class="project-item-text">{{ getGitStatus(project) }}</p> <p class="project-item-text">{{ getGitStatus(project) }}</p>
@ -408,9 +427,12 @@
{{ $t('copy git url') }} {{ $t('copy git url') }}
</button> </button>
</div> </div>
<div class="project-code-statistics-container"> <!-- <div class="project-code-statistics-container">
<button class="project-code-manage-button">{{ $t('Manage') }}</button> <button class="project-code-manage-button">{{ $t('Manage') }}</button>
{{ $t('TO BE IMPLEMENTED.') }} {{ $t('TO BE IMPLEMENTED.') }}
</div> -->
<div class="chart-container">
<v-chart :option="currentChartData" autoresize />
</div> </div>
</div> </div>
</div> </div>
@ -425,7 +447,7 @@
aria-expanded="false" aria-expanded="false"
aria-controls="collapse-project-issue" aria-controls="collapse-project-issue"
> >
<div class="project-issue-bar-container"> <div class="project-issue-bar-container dashed-container">
<div class="project-issue-open-issues"> <div class="project-issue-open-issues">
<label class="project-item-label">{{ $t('Open issues') }}</label> <label class="project-item-label">{{ $t('Open issues') }}</label>
<p class="project-item-text"> <p class="project-item-text">
@ -452,11 +474,7 @@
class="accordion-collapse collapse" class="accordion-collapse collapse"
data-bs-parent="#collapse-project-issue" data-bs-parent="#collapse-project-issue"
> >
<!-- <div class="project-issue-statistics-container"> <div class="project-invite-collaborator-containter">
<button class="project-issue-manage-button">Manage</button>
TO BE IMPLEMENTED.
</div> -->
<div class="project-new-issue-containter">
<button <button
class="accordion-button collapsed" class="accordion-button collapsed"
type="button" type="button"
@ -465,7 +483,7 @@
aria-expanded="false" aria-expanded="false"
aria-controls="collapse-project-issue" aria-controls="collapse-project-issue"
> >
<div class="project-add-new-issue">+ {{ $t('Add Issue') }}</div> <div class="project-invite-collaborator">+ {{ $t('Add Issue') }}</div>
</button> </button>
<div <div
id="collapse-project-new-issue" id="collapse-project-new-issue"
@ -477,15 +495,8 @@
<label class="project-item-label">{{ <label class="project-item-label">{{
$t('New issue description') $t('New issue description')
}}</label> }}</label>
<textarea
class="project-new-issue-textarea"
type="text"
v-model="newIssueDescriptions[project_index]"
/>
</div>
<div class="project-new-issue-action-container">
<button <button
class="project-new-issue-action-button" class="project-issue-description-btn"
@click=" @click="
postNewIssue( postNewIssue(
project.request.product_id, project.request.product_id,
@ -497,10 +508,14 @@
{{ $t('Submit') }} {{ $t('Submit') }}
</button> </button>
</div> </div>
<textarea type="text" v-model="newIssueDescriptions[project_index]" />
<!-- <div class="project-new-issue-action-container">
</div> -->
</div> </div>
</div> </div>
</div> </div>
<div class="project-issues-containter"> <div>
<div <div
class="project-issue-container" class="project-issue-container"
v-for="(issue, issue_index) in project.project.issue.open_issues" v-for="(issue, issue_index) in project.project.issue.open_issues"
@ -517,6 +532,7 @@
aria-expanded="false" aria-expanded="false"
aria-controls="collapse-project-issue-details" aria-controls="collapse-project-issue-details"
> >
<div class="project-issue-header dashed-container">
<div class="project-issue-title"> <div class="project-issue-title">
<label class="project-item-label">{{ $t('Issue title') }}</label> <label class="project-item-label">{{ $t('Issue title') }}</label>
<p class="project-item-text">{{ issue.title }}</p> <p class="project-item-text">{{ issue.title }}</p>
@ -533,6 +549,7 @@
{{ getDateFromFulltimeString(issue.update_time) }} {{ getDateFromFulltimeString(issue.update_time) }}
</p> </p>
</div> </div>
</div>
</button> </button>
</h2> </h2>
<div <div
@ -541,34 +558,38 @@
:data-bs-parent="'#collapse-project-issue-details' + issue_index" :data-bs-parent="'#collapse-project-issue-details' + issue_index"
> >
<div class="project-issue-description-container"> <div class="project-issue-description-container">
<div class="project-issue-description">
<label class="project-item-label">{{ <label class="project-item-label">{{
$t('Issue description') $t('Issue description')
}}</label> }}</label>
<p class="project-item-text">{{ issue.description }}</p>
</div>
<div class="project-issue-action-container">
<button <button
:hidden="!showIssueActionButton(project.project, issue, 'Resolve')" :hidden="
class="project-issue-action-button" !showIssueActionButton(project.project, issue, 'Resolve')
"
class="project-issue-description-btn"
@click="setProjectIssueStatus(issue.id, 1)" @click="setProjectIssueStatus(issue.id, 1)"
> >
{{ $t('Resolve') }} {{ $t('Resolve') }}
</button> </button>
<button <button
:hidden="!showIssueActionButton(project.project, issue, 'Confirm')" :hidden="
class="project-issue-action-button" !showIssueActionButton(project.project, issue, 'Confirm')
"
class="project-issue-description-btn"
@click="setProjectIssueStatus(issue.id, 2)" @click="setProjectIssueStatus(issue.id, 2)"
> >
{{ $t('Confirm') }} {{ $t('Confirm') }}
</button> </button>
<button <button
:hidden="!showIssueActionButton(project.project, issue, 'Reopen')" :hidden="!showIssueActionButton(project.project, issue, 'Reopen')"
class="project-issue-action-button" class="project-issue-description-btn"
@click="setProjectIssueStatus(issue.id, 0)" @click="setProjectIssueStatus(issue.id, 0)"
> >
{{ $t('Reopen') }} {{ $t('Reopen') }}
</button> </button>
</div> </div>
<p class="project-item-text">{{ issue.description }}</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -603,8 +624,15 @@ import {
convertIntoToMilestoneStatus, convertIntoToMilestoneStatus,
convertIntoToProjectIssueStatus convertIntoToProjectIssueStatus
} from '@/types/index' } from '@/types/index'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart } from 'echarts/charts'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import VChart from 'vue-echarts'
use([CanvasRenderer, LineChart, LegendComponent, GridComponent, TooltipComponent])
export default { export default {
name: 'Workspace', name: 'Workspace',
components: { VChart },
props: {}, props: {},
mounted() { mounted() {
this.fetchView() this.fetchView()
@ -620,7 +648,26 @@ export default {
imageUrl: null, imageUrl: null,
index: null index: null
}, },
currentChartData: {
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line',
areaStyle: { color: 'rgba(63,73,255,0.1)' },
smooth: true,
symbol: 'none',
connectNulls: true
}
]
},
downstream_web_socket: null downstream_web_socket: null
} }
}, },
@ -670,14 +717,14 @@ export default {
getTitleForItemInfo(project) { getTitleForItemInfo(project) {
switch (project.status) { switch (project.status) {
case projectStatusEnum.RECRUITING: case projectStatusEnum.RECRUITING:
return 'Proposals' return this.$t('Proposals')
case projectStatusEnum.PENDING: case projectStatusEnum.PENDING:
case projectStatusEnum.REJECTED: case projectStatusEnum.REJECTED:
return 'Issuer' return this.$t('Issuer')
case projectStatusEnum.ONGOING: case projectStatusEnum.ONGOING:
return 'Milestone(s)' return this.$t('Milestone(s)')
default: default:
return 'Information' return this.$t('Information')
} }
}, },
getContentForItemInfo(project) { getContentForItemInfo(project) {
@ -779,6 +826,10 @@ export default {
WorksapceApi.setMillestoneStatus(project.id, milestone.index, milestoneStatusEnum.PAID) WorksapceApi.setMillestoneStatus(project.id, milestone.index, milestoneStatusEnum.PAID)
.then((response) => { .then((response) => {
this.fetchView() this.fetchView()
this.qrCode = {
imageUrl: null,
index: null
}
}) })
.catch((error) => { .catch((error) => {
this.mnx_backendErrorHandler(error) this.mnx_backendErrorHandler(error)
@ -786,25 +837,25 @@ export default {
}, },
fetchMilestoneActionButtonText(project, milestone) { fetchMilestoneActionButtonText(project, milestone) {
if (milestone.index < project.current_milestone) { if (milestone.index < project.current_milestone) {
return 'Operation Complete' return this.$t('Operation Complete')
} else if (milestone.index === project.current_milestone) { } else if (milestone.index === project.current_milestone) {
if (milestone.status === milestoneStatusEnum.IMPLEMENTING) { if (milestone.status === milestoneStatusEnum.IMPLEMENTING) {
if (project.issuers.includes(project.current_user_id)) { if (project.issuers.includes(project.current_user_id)) {
return 'Wait for completion' return this.$t('Wait for completion')
} else { } else {
return 'Mission complete' return this.$t('Mission complete')
} }
} else if (milestone.status === milestoneStatusEnum.OUTSTANDING) { } else if (milestone.status === milestoneStatusEnum.OUTSTANDING) {
if (project.issuers.includes(project.current_user_id)) { if (project.issuers.includes(project.current_user_id)) {
return 'Payment' return this.$t('Payment')
} else { } else {
return 'Wait for payment' return this.$t('Wait for payment')
} }
} else if (milestone.status === milestoneStatusEnum.PAID) { } else if (milestone.status === milestoneStatusEnum.PAID) {
if (project.issuers.includes(project.current_user_id)) { if (project.issuers.includes(project.current_user_id)) {
return 'Wait for confirmation' return this.$t('Wait for confirmation')
} else { } else {
return 'Confirm receipt' return this.$t('Confirm receipt')
} }
} }
} else { } else {
@ -812,7 +863,7 @@ export default {
} }
}, },
getGitStatus(project) { getGitStatus(project) {
return project.project?.code?.git_url ? 'Ready' : 'Not available' return project.project?.code?.git_url ? this.$t('Ready') : this.$t('Not available')
}, },
copyCodeGit(project) { copyCodeGit(project) {
if (project.project.code.git_url) { if (project.project.code.git_url) {
@ -888,7 +939,7 @@ export default {
postNewIssue(product_id, project_id, issue_description) { postNewIssue(product_id, project_id, issue_description) {
if (issue_description == null || issue_description == '') { if (issue_description == null || issue_description == '') {
alert('Please input issue description') alert(this.$t('Please input issue description'))
return return
} }
WorksapceApi.postIssueForProduct(product_id, project_id, issue_description) WorksapceApi.postIssueForProduct(product_id, project_id, issue_description)
@ -911,17 +962,17 @@ export default {
showIssueActionButton(project, issue, button_text) { showIssueActionButton(project, issue, button_text) {
switch (issue.status) { switch (issue.status) {
case projectIssueStatusEnum.OPEN: case projectIssueStatusEnum.OPEN:
if (button_text === 'Resolve') { if (button_text === this.$t('Resolve')) {
return project.providers.includes(project.current_user_id) return project.providers.includes(project.current_user_id)
} }
break break
case 1: case 1:
if (button_text === 'Reopen' || button_text === 'Confirm') { if (button_text === this.$t('Reopen') || button_text === this.$t('Confirm')) {
return project.issuers.includes(project.current_user_id) return project.issuers.includes(project.current_user_id)
} }
break break
case projectIssueStatusEnum.CLOSED: case projectIssueStatusEnum.CLOSED:
if (button_text === 'Reopen') { if (button_text === this.$t('Reopen')) {
return project.issuers.includes(project.current_user_id) return project.issuers.includes(project.current_user_id)
} }
break break
@ -1219,6 +1270,76 @@ export default {
@extend .initiate-button; @extend .initiate-button;
} }
.project-milestones-table {
border: 0;
border-collapse: collapse;
width: 100%;
.project-milestones-table-content {
padding: 12px;
text-align: left;
display: flex;
flex-direction: column;
.project-milestones-table-label {
font-size: 12px;
color: #666666;
line-height: 1;
margin-bottom: 3px;
}
.project-milestones-table-span {
font-size: 14px;
font-weight: bold;
color: #242424;
line-height: 1;
}
.btn-link {
padding: 0;
width: fit-content;
}
}
.project-milestones-qrcode-content {
padding: 32px 12px;
display: flex;
flex-direction: column;
align-items: center;
img {
width: 154px;
border-radius: 12px;
overflow: hidden;
margin-bottom: 10px;
}
.project-milestones-qrcode-button {
padding: 2px 6px;
font-size: 14px;
color: $primary;
font-weight: bold;
border: 1px solid $primary;
background-color: #f3f6ff;
box-shadow: none;
border-radius: 2px;
}
}
tr {
border-bottom: 1px solid #e1e1e1;
td:first-child {
.project-milestones-table-content {
padding-left: 24px;
}
}
td:last-child {
.project-milestones-table-content {
padding-right: 24px;
}
}
}
}
.project-milestones-containter { .project-milestones-containter {
@extend .container; @extend .container;
@extend .border; @extend .border;
@ -1260,12 +1381,13 @@ export default {
.project-code-git-url-container { .project-code-git-url-container {
@extend .flex-row-container; @extend .flex-row-container;
@extend .justify-content-start; @extend .justify-content-end;
@extend .my-3; @extend .my-3;
} }
.project-code-copy-git-url { .project-code-copy-git-url {
@extend .initiate-button; @extend .btn;
@extend .btn-link;
} }
.project-code-statistics-container { .project-code-statistics-container {
@ -1304,8 +1426,33 @@ export default {
} }
.project-issue-description-container { .project-issue-description-container {
@extend .text-start; padding: 12px;
@extend .flex-grow-1;
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 { .project-issue-action-container {
@ -1313,7 +1460,8 @@ export default {
} }
.project-issue-action-button { .project-issue-action-button {
@extend .initiate-button; @extend .btn;
@extend .btn-link;
@extend .float-end; @extend .float-end;
} }
@ -1323,15 +1471,14 @@ export default {
@extend .p-3; @extend .p-3;
} }
.project-issues-containter {
@extend .container;
@extend .border;
}
.project-issue-container { .project-issue-container {
@extend .justify-content-between; @extend .justify-content-between;
} }
.project-issue-header {
display: flex;
}
.project-issue-title { .project-issue-title {
@extend .text-start; @extend .text-start;
@extend .flex-grow-1; @extend .flex-grow-1;
@ -1357,20 +1504,58 @@ export default {
width: 6vw; width: 6vw;
} }
.inline-accordion-body {
padding: 0 !important;
}
.project-invite-collaborator-containter { .project-invite-collaborator-containter {
@extend .container; border-bottom: 1px solid #e1e1e1;
@extend .border;
.accordion-button {
// border: 1px solid $primary;
// color: $primary;
// background-color: #F3F6FF;
padding: 12px !important;
&::after {
display: none;
}
}
} }
.project-invite-collaborator { .project-invite-collaborator {
width: 100%; width: 100%;
text-align: center; text-align: center;
@extend .initiate-button; @extend .initiate-button;
color: $primary;
background-color: #f3f6ff;
} }
.project-invite-collaborator-form-container { .project-invite-collaborator-form-container {
@extend .container; margin: 12px;
height: 100px; 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 { .project-invite-collaborator-form {
@ -1386,4 +1571,8 @@ export default {
@extend .initiate-button; @extend .initiate-button;
@extend .float-end; @extend .float-end;
} }
.chart-container {
width: 100%;
height: 357px;
}
</style> </style>

View File

@ -10,10 +10,6 @@
</template> </template>
<script> <script>
import {
UserProfileApi
// requestHubUtils,
} from '@/utils/index'
export default { export default {
name: 'RquestIssueDeposit', name: 'RquestIssueDeposit',
props: { props: {

View File

@ -8,10 +8,6 @@
</template> </template>
<script> <script>
import {
UserProfileApi
// userProfileValidator,
} from '@/utils/index'
export default { export default {
name: 'RquestIssueDeposited', name: 'RquestIssueDeposited',
props: { props: {

View File

@ -91,7 +91,7 @@
<script> <script>
import { WorksapceApi, requestIssueUtils, DateUtils } from '@/utils/index' import { WorksapceApi, requestIssueUtils, DateUtils } from '@/utils/index'
import { requestStatusEnum, convertIntoToRequestStatus } from '@/types/index' import { convertIntoToRequestStatus } from '@/types/index'
export default { export default {
name: 'RequestManage', name: 'RequestManage',
props: { props: {

View File

@ -52,7 +52,7 @@
> >
<div class="execution-duration-containter" id="execution-duration-containter"> <div class="execution-duration-containter" id="execution-duration-containter">
<span class="execution-duration-span" id="execution-duration-span"> <span class="execution-duration-span" id="execution-duration-span">
{{ proposal.duration_in_day }} day(s)</span {{ proposal.duration_in_day }} {{ $t('day(s)') }}</span
> >
</div> </div>
</div> </div>

View File

@ -66,6 +66,9 @@ import HeaderUser from '@/headers/HeaderUser.vue'
//Lab //Lab
import LabHome from '@/pages/lab/Home.vue' import LabHome from '@/pages/lab/Home.vue'
import TranslationHome from '@/pages/lab/translation/Home.vue' import TranslationHome from '@/pages/lab/translation/Home.vue'
import TaskCompletion from '@/pages/lab/openai/TaskCompletion.vue'
import MultiturnChat from '@/pages/lab/openai/MultiturnChat.vue'
import ImageGeneration from '@/pages/lab/openai/ImageGeneration.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
@ -389,6 +392,24 @@ const router = createRouter({
path: '/machine-translation', path: '/machine-translation',
meta: { requiredRoles: [userRoleEnum.PERSONAL] }, meta: { requiredRoles: [userRoleEnum.PERSONAL] },
components: { default: TranslationHome, footer: FooterUser, header: HeaderUser } components: { default: TranslationHome, footer: FooterUser, header: HeaderUser }
},
{
name: 'task-completion',
path: '/task-completion',
meta: { requiredRoles: [userRoleEnum.PERSONAL] },
components: { default: TaskCompletion, footer: FooterUser, header: HeaderUser }
},
{
name: 'multiturn-chat',
path: '/multiturn-chat',
meta: { requiredRoles: [userRoleEnum.PERSONAL] },
components: { default: MultiturnChat, footer: FooterUser, header: HeaderUser }
},
{
name: 'image-generation',
path: '/image-generation',
meta: { requiredRoles: [userRoleEnum.PERSONAL] },
components: { default: ImageGeneration, footer: FooterUser, header: HeaderUser }
} }
], ],

View File

@ -1,13 +1,18 @@
/* eslint-disable no-prototype-builtins */ /* eslint-disable no-prototype-builtins */
import { i18n } from '@/lang' import { i18n } from '@/lang'
import { WsConnectionFactory } from '@/utils/backend/websocket' import { WsConnectionFactory } from '@/utils/backend/websocket'
import { MessageHubApi } from '@/utils/backend/messageHub'
const ignoreEventType = ['test']
const GWT = new Date('01 Jan 1970 00:00:00 GMT').toISOString()
const basicStore = { const basicStore = {
namespaced: true, namespaced: true,
state() { state() {
return { return {
language: 'zh', language: 'zh',
unreadCountMapper: [], conversations: [],
unreadConversationCount: 0,
downstream_web_socket: null downstream_web_socket: null
} }
}, },
@ -23,20 +28,59 @@ const basicStore = {
() => { () => {
// keep // keep
setInterval(() => { setInterval(() => {
state.downstream_web_socket.send(1) state.downstream_web_socket.send('keep alive')
}, 1000 * 60) }, 1000 * 60)
console.log('downstream_web_socket open') console.log('downstream_web_socket open')
}, },
(e) => { (e) => {
const data = JSON.parse(e.data) const data = JSON.parse(e.data)
let unread = state.unreadCountMapper[data.sender_id] // console.log('downstream_web_socket onmessage: ', data)
if (unread) { if (ignoreEventType.indexOf(data.event) !== -1) {
unread++ return
} else {
unread = 1
} }
state.unreadCountMapper[data.sender_id] = unread
console.log('downstream_web_socket onmessage: ', data, state.unreadCountMapper) if (data.event === 'connected') {
// 读取缓存
let local_conversations, local_unreadConversationCount
try {
local_conversations = JSON.parse(localStorage.getItem('conversations'))
local_unreadConversationCount = localStorage.getItem('unreadConversationCount')
} catch (error) {
console.log('local error', error)
}
if (local_conversations) {
state.conversations = local_conversations
state.unreadConversationCount = local_unreadConversationCount || 0
return
}
}
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)
})
}
})
}, },
() => { () => {
console.log('downstream_web_socket error') console.log('downstream_web_socket error')
@ -46,8 +90,17 @@ const basicStore = {
} }
) )
}, },
readMessageBy(state, sender) { readMessageBy(state, conversation_id) {
delete state.unreadCountMapper?.[sender] for (let i = 0; i < state.conversations.length; i++) {
if (conversation_id === state.conversations[i].id) {
const nsc = Object.assign([], state.conversations)
nsc[i].unread = false
state.conversations = nsc
if (state.unreadConversationCount > 0) {
state.unreadConversationCount = state.unreadConversationCount - 1
}
}
}
} }
}, },
actions: { actions: {
@ -65,17 +118,11 @@ const basicStore = {
language(state) { language(state) {
return state.language return state.language
}, },
unreadCount(state) { unreadConversationCount(state) {
let count = 0 return state.unreadConversationCount
for (let key in state.unreadCountMapper) {
if (state.unreadCountMapper.hasOwnProperty(key)) {
count += state.unreadCountMapper[key]
}
}
return count
}, },
unreadCountMapper(state) { conversations(state) {
return state.unreadCountMapper return state.conversations
} }
} }
} }

View File

@ -20,7 +20,11 @@ class ContentApi {
return request return request
} }
static retrieve_career_directories(host) { static retrieve_career_directories(host) {
const request = backendAxios.post('/api/content/retrieve-career-directories', { host: host }, {}) const request = backendAxios.post(
'/api/content/retrieve-career-directories',
{ host: host },
{}
)
return request return request
} }
static retrieve_contact_directories(host) { static retrieve_contact_directories(host) {

View File

@ -14,5 +14,45 @@ class LabApi {
) )
return request return request
} }
static task_completion(prompt) {
let jwt = userUtils.getJwtToken()
const request = backendAxios.post(
'/api/lab/openai-completion',
{
user_prompt: prompt
},
{
headers: { Authorization: `Bearer ${jwt}` }
}
)
return request
}
static image_generation(prompt) {
let jwt = userUtils.getJwtToken()
const request = backendAxios.post(
'/api/lab/generate-image',
{
user_prompt: prompt
},
{
headers: { Authorization: `Bearer ${jwt}` }
}
)
return request
}
static multiturn_chat(messages) {
let jwt = userUtils.getJwtToken()
const request = backendAxios.post(
'/api/lab/openai-chat',
{
messages: messages
},
{
headers: { Authorization: `Bearer ${jwt}` }
}
)
return request
}
} }
export { LabApi } export { LabApi }

View File

@ -1,9 +1,9 @@
import { backendAxios } from './axios' import { backendAxios } from './axios'
import { userUtils } from '../store/index' // import { userUtils } from '../store/index'
class MessageHubApi { class MessageHubApi {
static fetchConversations(last_update_time) { static fetchConversations(last_update_time, jwt) {
let jwt = userUtils.getJwtToken() // let jwt = userUtils.getJwtToken()
const request = backendAxios.post( const request = backendAxios.post(
'/api/messages/fetch-conversations-for-user', '/api/messages/fetch-conversations-for-user',
{ {
@ -15,8 +15,8 @@ class MessageHubApi {
) )
return request return request
} }
static fetchMessages(conversation_id, last_update_time) { static fetchMessages(conversation_id, last_update_time, jwt) {
let jwt = userUtils.getJwtToken() // let jwt = userUtils.getJwtToken()
const request = backendAxios.post( const request = backendAxios.post(
'/api/messages/fetch-message-thread-for-conversation', '/api/messages/fetch-message-thread-for-conversation',
{ {
@ -30,8 +30,8 @@ class MessageHubApi {
return request return request
} }
static sendMessageToConversation(conversation_id, message) { static sendMessageToConversation(conversation_id, message, jwt) {
let jwt = userUtils.getJwtToken() // let jwt = userUtils.getJwtToken()
const request = backendAxios.post( const request = backendAxios.post(
'/api/messages/send-message-to-conversation', '/api/messages/send-message-to-conversation',
{ {

View File

@ -8,6 +8,7 @@ class WsConnectionFactory {
this.socket.onmessage = onMessage this.socket.onmessage = onMessage
this.socket.onerror = onError this.socket.onerror = onError
this.socket.onclose = onClose this.socket.onclose = onClose
return this.socket
} }
} }

View File

@ -1,2 +1,3 @@
export { passwordValidator } from './passwordValidator' export { passwordValidator } from './passwordValidator'
export { applicantValidator } from './applicantValidator' export { applicantValidator } from './applicantValidator'
export { websiteValidator } from './websiteValidator'

View File

@ -22,5 +22,5 @@ class WebsiteValidator extends TextValidator {
) )
} }
} }
const websiteValidator = new WebsiteValidator()
export { WebsiteValidator } export { websiteValidator }