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",
"bootstrap": "^5.3.1",
"buffer": "^6.0.3",
"echarts": "^5.5.1",
"pdfjs-dist": "^4.3.136",
"pinia": "^2.1.6",
"vue": "^3.3.4",
"vue-echarts": "^6.7.3",
"vue-i18n": "^9.13.1",
"vue-router": "^4.2.4",
"vuex": "^4.1.0"

View File

@ -11,88 +11,104 @@
@mouseup.stop="selectionEnd"
/>
<div v-if="!disabled" class="editor-control" :style="editorCtrlStyle">
<div v-for="(item, index) in iconList" :key="index" class="editor-item">
<button
class="item-icon"
:class="{
activity: commandStates.indexOf(item.type) !== -1,
last: index === iconList.length - 1
}"
:data-info="item.name"
@click="iconClick($event, item.type)"
:data-bs-toggle="item.drop ? 'dropdown' : ''"
data-bs-auto-close="outside"
aria-expanded="false"
>
<svg-icon :icon="item.icon" class-name="icon" />
</button>
<div class="dropmenu drop-style" v-if="item.type === 'style'">
<ul>
<li>
<a href="#" @click="iconClick($event, 'p', 'style')"><p>Text</p></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'pre', 'style')"><pre>code</pre></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'blockquote', 'style')"
><blockquote>引用</blockquote></a
>
</li>
<li>
<a href="#" @click="iconClick($event, 'h1', 'style')"><h1>Caption 1</h1></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'h2', 'style')"><h2>Caption 2</h2></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'h3', 'style')"><h3>Caption 3</h3></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'h4', 'style')"><h4>Caption 4</h4></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'h5', 'style')"><h5>Caption 5</h5></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'h6', 'style')"><h6>Caption 6</h6></a>
</li>
</ul>
</div>
<div class="dropmenu drop-style" v-if="item.type === 'alignjustify'">
<ul>
<li>
<a href="#" @click="iconClick($event, 'justifyCenter', 'alignjustify')">
<i class="iconfont icon-aligncenter"></i>
<span>middle</span>
</a>
</li>
<li>
<a href="#" @click="iconClick($event, 'justifyLeft', 'alignjustify')">
<i class="iconfont icon-alignleft"></i>
<span>left aligned</span>
</a>
</li>
<li>
<a href="#" @click="iconClick($event, 'justifyRight', 'alignjustify')">
<i class="iconfont icon-alignright"></i>
<span>right aligned</span>
</a>
</li>
<li>
<a href="#" @click="iconClick($event, 'justifyFull', 'alignjustify')">
<i class="iconfont icon-alignjustify"></i>
<span>default aligned</span>
</a>
</li>
</ul>
</div>
</div>
<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">
<button
class="item-icon"
:class="{
activity: commandStates.indexOf(item.type) !== -1,
last: index === iconList.length - 1
}"
:data-info="item.name"
@click="iconClick($event, item.type)"
:data-bs-toggle="item.drop ? 'dropdown' : ''"
data-bs-auto-close="outside"
aria-expanded="false"
>
<svg-icon :icon="item.icon" class-name="icon" />
</button>
<div class="dropmenu drop-style" v-if="item.type === 'style'">
<ul>
<li>
<a href="#" @click="iconClick($event, 'p', 'style')"><p>Text</p></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'pre', 'style')"><pre>code</pre></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'blockquote', 'style')"
><blockquote>引用</blockquote></a
>
</li>
<li>
<a href="#" @click="iconClick($event, 'h1', 'style')"><h1>Caption 1</h1></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'h2', 'style')"><h2>Caption 2</h2></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'h3', 'style')"><h3>Caption 3</h3></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'h4', 'style')"><h4>Caption 4</h4></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'h5', 'style')"><h5>Caption 5</h5></a>
</li>
<li>
<a href="#" @click="iconClick($event, 'h6', 'style')"><h6>Caption 6</h6></a>
</li>
</ul>
</div>
<div class="dropmenu drop-style" v-if="item.type === 'alignjustify'">
<ul>
<li>
<a href="#" @click="iconClick($event, 'justifyCenter', 'alignjustify')">
<i class="iconfont icon-aligncenter"></i>
<span>middle</span>
</a>
</li>
<li>
<a href="#" @click="iconClick($event, 'justifyLeft', 'alignjustify')">
<i class="iconfont icon-alignleft"></i>
<span>left aligned</span>
</a>
</li>
<li>
<a href="#" @click="iconClick($event, 'justifyRight', 'alignjustify')">
<i class="iconfont icon-alignright"></i>
<span>right aligned</span>
</a>
</li>
<li>
<a href="#" @click="iconClick($event, 'justifyFull', 'alignjustify')">
<i class="iconfont icon-alignjustify"></i>
<span>default aligned</span>
</a>
</li>
</ul>
</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>
</template>
<script>
import SvgIcon from '@/components/SvgIcon.vue'
import { websiteValidator } from '@/utils/validator'
export default {
name: 'FreeleapsEditor',
props: {
@ -112,6 +128,8 @@ export default {
selectedRange: '',
editorCtrlStyle: {},
commandStates: [],
inputing: false,
linkUrl: '',
iconList: [
// {
// name: 'lable', // hover name
@ -175,6 +193,13 @@ export default {
icon: 'fe-orderedlist',
drop: false,
canChoose: true
},
{
name: 'link',
type: 'link',
icon: 'fe-link',
drop: false,
canChoose: true
}
// {
// name: 'align-justify',
@ -202,6 +227,8 @@ export default {
} else {
this.selectedRange = null
this.editorCtrlStyle = { display: 'none' }
this.inputing = false
this.linkUrl = ''
// this.commandStates = []
}
},
@ -225,6 +252,10 @@ export default {
this.$refs.editor.focus()
this.selectedRange = this.getSelect()
this.restoreSelection()
if (type === 'link') {
this.inputing = true
return
}
this.changeStyle(type)
this.$nextTick(() => {
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() {
if (window.getSelection) {
let sel = window.getSelection()
@ -417,6 +468,30 @@ export default {
justify-content: center;
user-select: none;
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 {
position: relative;
width: 50px;
@ -486,6 +561,9 @@ export default {
&.activity + .dropmenu {
display: block;
}
&.item-icon-back {
color: black;
}
}
}
</style>
@ -521,4 +599,23 @@ export default {
.editor-body ol {
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>

View File

@ -4,7 +4,7 @@
<div
class="information-bar"
@click="gotoMessages"
:class="{ active: activePath == 'message', unread: unreadCount > 0 }"
:class="{ active: activePath == 'message', unread: unreadConversationCount > 0 }"
>
<img alt="freeleaps logo" src="@/assets/message.png" />
</div>
@ -15,7 +15,7 @@
:class="activePath == 'Workspace' ? 'active' : ''"
>
<svg-icon icon="workspace" class-name="icon" />
Workspace
{{ $t('Workspace') }}
</button>
<button
class="navigation-item"
@ -23,7 +23,7 @@
:class="activePath == 'Requests' ? 'active' : ''"
>
<svg-icon icon="requests" class-name="icon" />
Requests
{{ $t('Requests') }}
</button>
<button
class="navigation-item"
@ -31,7 +31,7 @@
:class="activePath == 'Providers' ? 'active' : ''"
>
<svg-icon icon="providers" class-name="icon" />
Providers
{{ $t('Providers') }}
</button>
<button
class="navigation-item"
@ -39,7 +39,7 @@
:class="activePath == 'Post' ? 'active' : ''"
>
<svg-icon icon="post" class-name="icon" />
Post
{{ $t('Post') }}
</button>
<div class="form-check form-switch header-switch-container">
<input
@ -50,10 +50,10 @@
disabled
/>
<label class="form-check-label" for="personal-earning-now-checkbox">
<span>Providing service</span>
<span>{{ $t('Providing service') }}</span>
</label>
<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>
<laguage-switch class="laguage-switch" />
@ -68,17 +68,17 @@
/>
<ul class="dropdown-menu" aria-labelledby="accountButton">
<li>
<button class="account-menu-button" @click="gotoProfile">Profile</button>
<button class="account-menu-button" @click="gotoProfile">{{ $t('Profile') }}</button>
</li>
<li>
<button class="account-menu-button" @click="gotoHistory">History</button>
<button class="account-menu-button" @click="gotoHistory">{{ $t('History') }}</button>
</li>
<li>
<hr class="dropdown-divider" />
</li>
<li>
<button class="account-menu-button" @click="signout">
Log out ({{ userIdentityNote }})
{{ $t('Log out') }} ({{ userIdentityNote }})
</button>
</li>
</ul>
@ -94,8 +94,8 @@ export default {
name: 'HeaderGuest',
components: { LaguageSwitch },
computed: {
unreadCount() {
return this.$store.getters['basic/unreadCount']
unreadConversationCount() {
return this.$store.getters['basic/unreadConversationCount']
}
},
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',
'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>
</template>
<script>
import cover_picture from '@/assets/images/lab-translation.png'
export default {
name: 'LabHome',
components: {},
@ -31,7 +32,28 @@ export default {
title_text: 'Machine Translation',
summary_text: 'Translate lanuages leverage AI power',
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>
<div class="directories_containter">
<div class="directory_container" v-for="(directory, index) in directories" :key="index"
@click="view_link(directory)">
<div
class="directory_container"
v-for="(directory, index) in directories"
:key="index"
@click="view_link(directory)"
>
<img class="directory_cover_image" :src="directory.cover_picture" />
<p class="directory-title">{{ directory.title_text }}</p>
<p class="directory-subtitle">{{ directory.summary_text }}</p>

View File

@ -1,6 +1,11 @@
<template>
<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>
<img class="blog_cover_image" :src="blog.cover_picture" />
<p v-text="retrieve_summary(blog)"></p>

View File

@ -1,6 +1,11 @@
<template>
<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">
{{ directory.title_text }}
</p>

View File

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

View File

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

View File

@ -34,7 +34,8 @@
>{{ $t('Stay on Freeleaps') }}</label
>
<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 class="provider-delivered-projects-container">
@ -101,7 +102,8 @@
$t('Project delivering time')
}}</label>
<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>
</div>
<div class="dd-project-container">
@ -445,11 +447,6 @@ export default {
border-top: 1px solid #dee2e6;
}
// .statistics-content-label {
// @extend .label-text-light;
// @extend .w-100;
// }
.statistics-content-container {
@extend .flex-colum-container;
padding: 0;

View File

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

View File

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

View File

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

View File

@ -207,7 +207,7 @@
></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">
<button
class="accordion-button collapsed"
@ -225,23 +225,15 @@
data-bs-parent="#collapse-project-invite-collaborator"
>
<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
class="project-invite-collaborator-input"
v-model="newInviteCollaborator[project_index]"
/>
</div>
<button
class="project-invite-collaborator-action-button"
@click="
<input
type="text"
v-model="newInviteCollaborator[project_index]"
:placeholder="$t('Input E-mail to invite other')"
@keydown.enter="
inviteCollaborator(project.project_id, newInviteCollaborator[project_index])
"
>
{{ $t('Submit') }}
</button>
/>
<svg-icon icon="msg-enter" class-name="project-invite-enter" />
</div>
</div>
</div>
@ -256,7 +248,7 @@
aria-expanded="false"
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">
<label class="project-item-label">{{ $t('Progress') }}</label>
<p class="project-item-text">
@ -296,65 +288,92 @@
class="accordion-collapse collapse"
data-bs-parent="#collapse-project-milestone"
>
<div class="project-milestones-containter">
<div
class="project-milestone-container"
<table class="project-milestones-table">
<tbody
v-for="milestone in project.project.progress.milestones"
:key="milestone.index"
:id="'project-milestone-' + milestone.index"
>
<div class="project-milestone-index">
<label class="project-item-label">{{ $t('Milestone') }}</label>
<p class="project-item-text">
{{ milestone.index }}
</p>
</div>
<div class="project-milestone-description">
<label class="project-item-label">{{ $t('Description') }}</label>
<p class="project-item-text">
{{ milestone.description }}
</p>
</div>
<div class="project-milestone-status">
<label class="project-item-label">{{ $t('Status') }}</label>
<p class="project-item-text">
{{ fromIntToMilestoneStatus(milestone.status) }}
</p>
</div>
<div class="project-milestone-payment">
<label class="project-item-label">{{ $t('Payment') }}</label>
<p class="project-item-text">
{{ milestone.actual_paid }} / {{ milestone.expected_payment }}
{{ project.project.progress.payment_currency }}
</p>
</div>
<div class="project-milestone-update">
<label class="project-item-label">{{ $t('Update') }}</label>
<p class="project-item-text">
{{ getDateFromFulltimeString(milestone.update_time) }}
</p>
</div>
<div class="project-milestone-action-button-container">
<button
class="project-milestone-action-button"
:disabled="isMilestoneActionButtonDisabled(project.project, milestone)"
:hidden="isMilestoneActionButtonHidden(project.project, milestone)"
@click="handleMilestoneAction(project.project, milestone)"
>
{{ fetchMilestoneActionButtonText(project.project, milestone) }}
</button>
</div>
<div v-if="this.qrCode.index === milestone.index">
<img width="100" height="100" :src="this.qrCode.imageUrl" />
<button
class="project-milestone-payment-confirm-button"
@click="handlePaymentAction(project.project, milestone)"
>
{{ $t('Mark As Paid') }}
</button>
</div>
</div>
</div>
<tr>
<td>
<div class="project-milestones-table-content">
<span class="project-milestones-table-label">{{
$t('Milestone')
}}</span>
<span class="project-milestones-table-span">{{
milestone.index
}}</span>
</div>
</td>
<td>
<div class="project-milestones-table-content">
<span class="project-milestones-table-label">{{
$t('Description')
}}</span>
<span class="project-milestones-table-span">{{
milestone.description
}}</span>
</div>
</td>
<td>
<div class="project-milestones-table-content">
<span class="project-milestones-table-label">{{ $t('Status') }}</span>
<span class="project-milestones-table-span">{{
fromIntToMilestoneStatus(milestone.status)
}}</span>
</div>
</td>
<td>
<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 }}
{{ project.project.progress.payment_currency }}
</span>
</div>
</td>
<td>
<div class="project-milestones-table-content">
<span class="project-milestones-table-label">{{ $t('Update') }}</span>
<span class="project-milestones-table-span">{{
getDateFromFulltimeString(milestone.update_time)
}}</span>
</div>
</td>
<td>
<div class="project-milestones-table-content">
<span class="project-milestones-table-label">{{ $t('Action') }}</span>
<button
class="btn btn-link"
:disabled="
isMilestoneActionButtonDisabled(project.project, milestone)
"
:hidden="isMilestoneActionButtonHidden(project.project, milestone)"
@click="handleMilestoneAction(project.project, milestone)"
>
{{ fetchMilestoneActionButtonText(project.project, milestone) }}
</button>
<!-- <span class="project-milestones-table-span">{{ getDateFromFulltimeString(milestone.update_time) }}</span> -->
</div>
</td>
</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
class="project-milestones-qrcode-button"
@click="handlePaymentAction(project.project, milestone)"
>
{{ $t('Mark As Paid') }}
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="accordion-item">
@ -367,7 +386,7 @@
aria-expanded="false"
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">
<label class="project-item-label">{{ $t('Code Depot') }}</label>
<p class="project-item-text">{{ getGitStatus(project) }}</p>
@ -408,9 +427,12 @@
{{ $t('copy git url') }}
</button>
</div>
<div class="project-code-statistics-container">
<!-- <div class="project-code-statistics-container">
<button class="project-code-manage-button">{{ $t('Manage') }}</button>
{{ $t('TO BE IMPLEMENTED.') }}
</div> -->
<div class="chart-container">
<v-chart :option="currentChartData" autoresize />
</div>
</div>
</div>
@ -425,7 +447,7 @@
aria-expanded="false"
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">
<label class="project-item-label">{{ $t('Open issues') }}</label>
<p class="project-item-text">
@ -452,11 +474,7 @@
class="accordion-collapse collapse"
data-bs-parent="#collapse-project-issue"
>
<!-- <div class="project-issue-statistics-container">
<button class="project-issue-manage-button">Manage</button>
TO BE IMPLEMENTED.
</div> -->
<div class="project-new-issue-containter">
<div class="project-invite-collaborator-containter">
<button
class="accordion-button collapsed"
type="button"
@ -465,7 +483,7 @@
aria-expanded="false"
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>
<div
id="collapse-project-new-issue"
@ -477,15 +495,8 @@
<label class="project-item-label">{{
$t('New issue description')
}}</label>
<textarea
class="project-new-issue-textarea"
type="text"
v-model="newIssueDescriptions[project_index]"
/>
</div>
<div class="project-new-issue-action-container">
<button
class="project-new-issue-action-button"
class="project-issue-description-btn"
@click="
postNewIssue(
project.request.product_id,
@ -497,10 +508,14 @@
{{ $t('Submit') }}
</button>
</div>
<textarea type="text" v-model="newIssueDescriptions[project_index]" />
<!-- <div class="project-new-issue-action-container">
</div> -->
</div>
</div>
</div>
<div class="project-issues-containter">
<div>
<div
class="project-issue-container"
v-for="(issue, issue_index) in project.project.issue.open_issues"
@ -517,21 +532,23 @@
aria-expanded="false"
aria-controls="collapse-project-issue-details"
>
<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 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>
@ -541,34 +558,38 @@
:data-bs-parent="'#collapse-project-issue-details' + issue_index"
>
<div class="project-issue-description-container">
<label class="project-item-label">{{
$t('Issue description')
}}</label>
<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 class="project-issue-action-container">
<button
:hidden="!showIssueActionButton(project.project, issue, 'Resolve')"
class="project-issue-action-button"
@click="setProjectIssueStatus(issue.id, 1)"
>
{{ $t('Resolve') }}
</button>
<button
:hidden="!showIssueActionButton(project.project, issue, 'Confirm')"
class="project-issue-action-button"
@click="setProjectIssueStatus(issue.id, 2)"
>
{{ $t('Confirm') }}
</button>
<button
:hidden="!showIssueActionButton(project.project, issue, 'Reopen')"
class="project-issue-action-button"
@click="setProjectIssueStatus(issue.id, 0)"
>
{{ $t('Reopen') }}
</button>
</div>
</div>
</div>
</div>
@ -603,8 +624,15 @@ import {
convertIntoToMilestoneStatus,
convertIntoToProjectIssueStatus
} 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 {
name: 'Workspace',
components: { VChart },
props: {},
mounted() {
this.fetchView()
@ -620,7 +648,26 @@ export default {
imageUrl: 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
}
},
@ -670,14 +717,14 @@ export default {
getTitleForItemInfo(project) {
switch (project.status) {
case projectStatusEnum.RECRUITING:
return 'Proposals'
return this.$t('Proposals')
case projectStatusEnum.PENDING:
case projectStatusEnum.REJECTED:
return 'Issuer'
return this.$t('Issuer')
case projectStatusEnum.ONGOING:
return 'Milestone(s)'
return this.$t('Milestone(s)')
default:
return 'Information'
return this.$t('Information')
}
},
getContentForItemInfo(project) {
@ -779,6 +826,10 @@ export default {
WorksapceApi.setMillestoneStatus(project.id, milestone.index, milestoneStatusEnum.PAID)
.then((response) => {
this.fetchView()
this.qrCode = {
imageUrl: null,
index: null
}
})
.catch((error) => {
this.mnx_backendErrorHandler(error)
@ -786,25 +837,25 @@ export default {
},
fetchMilestoneActionButtonText(project, milestone) {
if (milestone.index < project.current_milestone) {
return 'Operation Complete'
return this.$t('Operation Complete')
} else if (milestone.index === project.current_milestone) {
if (milestone.status === milestoneStatusEnum.IMPLEMENTING) {
if (project.issuers.includes(project.current_user_id)) {
return 'Wait for completion'
return this.$t('Wait for completion')
} else {
return 'Mission complete'
return this.$t('Mission complete')
}
} else if (milestone.status === milestoneStatusEnum.OUTSTANDING) {
if (project.issuers.includes(project.current_user_id)) {
return 'Payment'
return this.$t('Payment')
} else {
return 'Wait for payment'
return this.$t('Wait for payment')
}
} else if (milestone.status === milestoneStatusEnum.PAID) {
if (project.issuers.includes(project.current_user_id)) {
return 'Wait for confirmation'
return this.$t('Wait for confirmation')
} else {
return 'Confirm receipt'
return this.$t('Confirm receipt')
}
}
} else {
@ -812,7 +863,7 @@ export default {
}
},
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) {
if (project.project.code.git_url) {
@ -888,7 +939,7 @@ export default {
postNewIssue(product_id, project_id, issue_description) {
if (issue_description == null || issue_description == '') {
alert('Please input issue description')
alert(this.$t('Please input issue description'))
return
}
WorksapceApi.postIssueForProduct(product_id, project_id, issue_description)
@ -911,17 +962,17 @@ export default {
showIssueActionButton(project, issue, button_text) {
switch (issue.status) {
case projectIssueStatusEnum.OPEN:
if (button_text === 'Resolve') {
if (button_text === this.$t('Resolve')) {
return project.providers.includes(project.current_user_id)
}
break
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)
}
break
case projectIssueStatusEnum.CLOSED:
if (button_text === 'Reopen') {
if (button_text === this.$t('Reopen')) {
return project.issuers.includes(project.current_user_id)
}
break
@ -1219,6 +1270,76 @@ export default {
@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 {
@extend .container;
@extend .border;
@ -1260,12 +1381,13 @@ export default {
.project-code-git-url-container {
@extend .flex-row-container;
@extend .justify-content-start;
@extend .justify-content-end;
@extend .my-3;
}
.project-code-copy-git-url {
@extend .initiate-button;
@extend .btn;
@extend .btn-link;
}
.project-code-statistics-container {
@ -1304,8 +1426,33 @@ export default {
}
.project-issue-description-container {
@extend .text-start;
@extend .flex-grow-1;
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 {
@ -1313,7 +1460,8 @@ export default {
}
.project-issue-action-button {
@extend .initiate-button;
@extend .btn;
@extend .btn-link;
@extend .float-end;
}
@ -1323,15 +1471,14 @@ export default {
@extend .p-3;
}
.project-issues-containter {
@extend .container;
@extend .border;
}
.project-issue-container {
@extend .justify-content-between;
}
.project-issue-header {
display: flex;
}
.project-issue-title {
@extend .text-start;
@extend .flex-grow-1;
@ -1357,20 +1504,58 @@ export default {
width: 6vw;
}
.inline-accordion-body {
padding: 0 !important;
}
.project-invite-collaborator-containter {
@extend .container;
@extend .border;
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 {
@extend .container;
height: 100px;
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 {
@ -1386,4 +1571,8 @@ export default {
@extend .initiate-button;
@extend .float-end;
}
.chart-container {
width: 100%;
height: 357px;
}
</style>

View File

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

View File

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

View File

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

View File

@ -52,7 +52,7 @@
>
<div class="execution-duration-containter" id="execution-duration-containter">
<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>

View File

@ -66,6 +66,9 @@ import HeaderUser from '@/headers/HeaderUser.vue'
//Lab
import LabHome from '@/pages/lab/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({
history: createWebHistory(),
@ -389,6 +392,24 @@ const router = createRouter({
path: '/machine-translation',
meta: { requiredRoles: [userRoleEnum.PERSONAL] },
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 */
import { i18n } from '@/lang'
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 = {
namespaced: true,
state() {
return {
language: 'zh',
unreadCountMapper: [],
conversations: [],
unreadConversationCount: 0,
downstream_web_socket: null
}
},
@ -23,20 +28,59 @@ const basicStore = {
() => {
// keep
setInterval(() => {
state.downstream_web_socket.send(1)
state.downstream_web_socket.send('keep alive')
}, 1000 * 60)
console.log('downstream_web_socket open')
},
(e) => {
const data = JSON.parse(e.data)
let unread = state.unreadCountMapper[data.sender_id]
if (unread) {
unread++
} else {
unread = 1
// console.log('downstream_web_socket onmessage: ', data)
if (ignoreEventType.indexOf(data.event) !== -1) {
return
}
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')
@ -46,8 +90,17 @@ const basicStore = {
}
)
},
readMessageBy(state, sender) {
delete state.unreadCountMapper?.[sender]
readMessageBy(state, conversation_id) {
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: {
@ -65,17 +118,11 @@ const basicStore = {
language(state) {
return state.language
},
unreadCount(state) {
let count = 0
for (let key in state.unreadCountMapper) {
if (state.unreadCountMapper.hasOwnProperty(key)) {
count += state.unreadCountMapper[key]
}
}
return count
unreadConversationCount(state) {
return state.unreadConversationCount
},
unreadCountMapper(state) {
return state.unreadCountMapper
conversations(state) {
return state.conversations
}
}
}

View File

@ -20,7 +20,11 @@ class ContentApi {
return request
}
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
}
static retrieve_contact_directories(host) {

View File

@ -14,5 +14,45 @@ class LabApi {
)
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 }

View File

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

View File

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

View File

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

View File

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