Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
P
poc-fe
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
poc
poc-fe
Commits
6d7b65ec
Commit
6d7b65ec
authored
Apr 10, 2025
by
nick zheng
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'beta' into 'master'
Beta See merge request
!203
parents
2bb401e2
a90eeca3
Hide whitespace changes
Inline
Side-by-side
Showing
25 changed files
with
2429 additions
and
99 deletions
+2429
-99
plugins.ts
build/plugins.ts
+2
-2
package.json
package.json
+1
-0
pnpm-lock.yaml
pnpm-lock.yaml
+31
-2
user.ts
src/apis/user.ts
+6
-0
h5-login-bg.png
src/assets/images/login/h5-login-bg.png
+0
-0
h5-bg.png
src/assets/images/share/h5-bg.png
+0
-0
custom-modal.vue
src/components/custom-modal/custom-modal.vue
+1
-1
en.yaml
src/locales/langs/en.yaml
+22
-0
zh-cn.yaml
src/locales/langs/zh-cn.yaml
+23
-0
zh-hk.yaml
src/locales/langs/zh-hk.yaml
+22
-0
guards.ts
src/router/guards.ts
+1
-1
base.ts
src/router/modules/base.ts
+10
-1
h5-login.vue
src/views/login/h5-login.vue
+626
-0
index.vue
src/views/login/index.vue
+11
-0
reset-password.vue
src/views/reset-password/reset-password.vue
+371
-0
custom-loading.vue
src/views/share/components/custom-loading.vue
+13
-7
mobile-page-header.vue
src/views/share/components/mobile-page-header.vue
+0
-51
continue-question.vue
src/views/share/components/mobile/continue-question.vue
+113
-0
footer-input.vue
src/views/share/components/mobile/footer-input.vue
+670
-0
message-item.vue
src/views/share/components/mobile/message-item.vue
+193
-0
message-list.vue
src/views/share/components/mobile/message-list.vue
+92
-0
page-header.vue
src/views/share/components/mobile/page-header.vue
+100
-0
preamble.vue
src/views/share/components/mobile/preamble.vue
+48
-0
share-application-mobile.vue
src/views/share/share-application-mobile.vue
+50
-34
locales.d.ts
types/locales.d.ts
+23
-0
No files found.
build/plugins.ts
View file @
6d7b65ec
...
...
@@ -4,7 +4,7 @@ import checker from 'vite-plugin-checker'
import
{
visualizer
}
from
'rollup-plugin-visualizer'
import
AutoImport
from
'unplugin-auto-import/vite'
import
Components
from
'unplugin-vue-components/vite'
import
{
NaiveUiResolver
}
from
'unplugin-vue-components/resolvers'
import
{
NaiveUiResolver
,
VantResolver
}
from
'unplugin-vue-components/resolvers'
import
VueI18nPlugin
from
'@intlify/unplugin-vue-i18n/vite'
import
UnoCSS
from
'unocss/vite'
import
VueJsx
from
'@vitejs/plugin-vue-jsx'
...
...
@@ -20,7 +20,7 @@ export function setupPlugins(isBuild: boolean, envConf: ViteEnv, pathResolve: (d
],
}),
Components
({
resolvers
:
[
NaiveUiResolver
()],
resolvers
:
[
NaiveUiResolver
()
,
VantResolver
()
],
}),
VueI18nPlugin
({
include
:
[
pathResolve
(
'./src/locales/langs/**'
)],
...
...
package.json
View file @
6d7b65ec
...
...
@@ -44,6 +44,7 @@
"
spark-md5
"
:
"^3.0.2"
,
"
type-fest
"
:
"^4.26.1"
,
"
validator
"
:
"^13.12.0"
,
"
vant
"
:
"^4.9.18"
,
"
vue
"
:
"^3.5.13"
,
"
vue-i18n
"
:
"^9.14.0"
,
"
vue-router
"
:
"^4.4.5"
...
...
pnpm-lock.yaml
View file @
6d7b65ec
...
...
@@ -92,6 +92,9 @@ importers:
validator
:
specifier
:
^13.12.0
version
:
13.12.0
vant
:
specifier
:
^4.9.18
version
:
4.9.18(vue@3.5.13(typescript@5.6.2))
vue
:
specifier
:
^3.5.13
version
:
3.5.13(typescript@5.6.2)
...
...
@@ -1249,6 +1252,14 @@ packages:
peerDependencies
:
vite
:
^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
'
@vant/popperjs@1.3.0'
:
resolution
:
{
integrity
:
sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==
}
'
@vant/use@1.6.0'
:
resolution
:
{
integrity
:
sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==
}
peerDependencies
:
vue
:
^3.0.0
'
@vitejs/plugin-vue-jsx@4.0.1'
:
resolution
:
{
integrity
:
sha512-7mg9HFGnFHMEwCdB6AY83cVK4A6sCqnrjFYF4WIlebYAQVVJ/sC/CiTruVdrRlhrFoeZ8rlMxY9wYpPTIRhhAg==
}
engines
:
{
node
:
^18.0.0 || >=20.0.0
}
...
...
@@ -3284,6 +3295,11 @@ packages:
resolution
:
{
integrity
:
sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==
}
engines
:
{
node
:
'
>=
0.10'
}
vant@4.9.18
:
resolution
:
{
integrity
:
sha512-1bmWv/G0xz45btPqSasgv4TUdTqCneyFfnrQJ3xgerGvfTC5aP/rpO4wJb5FItCZjaSw5+9FNBj2Tz6n9TLXYA==
}
peerDependencies
:
vue
:
^3.0.0
vdirs@0.1.8
:
resolution
:
{
integrity
:
sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==
}
peerDependencies
:
...
...
@@ -4530,6 +4546,12 @@ snapshots:
-
rollup
-
supports-color
'
@vant/popperjs@1.3.0'
:
{}
'
@vant/use@1.6.0(vue@3.5.13(typescript@5.6.2))'
:
dependencies
:
vue
:
3.5.13(typescript@5.6.2)
'
@vitejs/plugin-vue-jsx@4.0.1(vite@5.4.6(@types/node@20.16.5)(sass@1.79.1)(terser@5.39.0))(vue@3.5.13(typescript@5.6.2))'
:
dependencies
:
'
@babel/core'
:
7.25.2
...
...
@@ -6514,7 +6536,7 @@ snapshots:
estree-walker
:
3.0.3
fast-glob
:
3.3.2
local-pkg
:
0.5.0
magic-string
:
0.30.1
1
magic-string
:
0.30.1
7
mlly
:
1.7.1
pathe
:
1.1.2
pkg-types
:
1.2.0
...
...
@@ -6603,7 +6625,7 @@ snapshots:
dependencies
:
browserslist
:
4.23.3
escalade
:
3.2.0
picocolors
:
1.1.
0
picocolors
:
1.1.
1
uri-js@4.4.1
:
dependencies
:
...
...
@@ -6613,6 +6635,13 @@ snapshots:
validator@13.12.0
:
{}
vant@4.9.18(vue@3.5.13(typescript@5.6.2))
:
dependencies
:
'
@vant/popperjs'
:
1.3.0
'
@vant/use'
:
1.6.0(vue@3.5.13(typescript@5.6.2))
'
@vue/shared'
:
3.5.13
vue
:
3.5.13(typescript@5.6.2)
vdirs@0.1.8(vue@3.5.13(typescript@5.6.2))
:
dependencies
:
evtd
:
0.2.4
...
...
src/apis/user.ts
View file @
6d7b65ec
...
...
@@ -42,3 +42,9 @@ export function fetchUserPasswordUpdate<T>(authCode: string, password: string) {
export
function
fetchGoogleClientId
<
T
>
()
{
return
request
.
post
<
T
>
(
'/googleConfigRest/getClientId.json'
)
}
export
function
fetchForgetMemberPassword
<
T
>
(
account
:
string
,
authCode
:
string
,
password
:
string
)
{
return
request
.
post
<
T
>
(
'/bizMemberInfoRest/forgetMemberPassword.json'
,
null
,
{
params
:
{
account
,
authCode
,
password
},
})
}
src/assets/images/login/h5-login-bg.png
0 → 100644
View file @
6d7b65ec
66.8 KB
src/assets/images/share/h5-bg.png
0 → 100644
View file @
6d7b65ec
13.1 KB
src/components/custom-modal/custom-modal.vue
View file @
6d7b65ec
...
...
@@ -29,7 +29,7 @@ const { t } = useI18n()
const
props
=
withDefaults
(
defineProps
<
Props
>
(),
{
height
:
240
,
width
:
500
,
borderRadius
:
6
,
borderRadius
:
10
,
btnLoading
:
false
,
btnDisabled
:
false
,
cancelBtnText
:
''
,
...
...
src/locales/langs/en.yaml
View file @
6d7b65ec
...
...
@@ -43,6 +43,7 @@ common_module:
publish_success_message
:
'
Release
success'
clear_success_message
:
'
Clear
successfully'
add_success_message
:
'
New
success'
reset_success_message
:
'
Reset
successful'
loading
:
'
Loading'
updating
:
'
Uploading'
successful_update
:
'
Update
successfully'
...
...
@@ -152,6 +153,7 @@ common_module:
not_certified_yet
:
'
Not
certified
yet'
authenticated
:
'
Authenticated'
cancel_authorization
:
'
Cancel
authorization'
get_code
:
'
Get
Code'
dialogue_module
:
continue_question_message
:
'
You
can
keep
asking
questions'
...
...
@@ -211,6 +213,7 @@ router_title_module:
order_manage
:
'
Order
Management'
data_statistic
:
'
Data
statistic'
plugin_center
:
'
Plugin
center'
reset_password
:
'
Reset
password'
login_module
:
app_welcome_words
:
'
Hi,
welcome
to
Model
Link'
...
...
@@ -227,6 +230,25 @@ login_module:
successful
:
'
Obtain
success'
get_verification_code
:
'
Get
verification
code'
other_login_methods
:
'
Other
login
methods'
mobile_welcome_words
:
'
Hello,
welcome
to
Model
Link!'
sign_in_with_mobile_number
:
'
Sign
in
with
mobile
number'
verification_code_login
:
'
Verification
code
login'
password_login
:
'
Password
login'
email_login
:
'
Email
login'
forgot_password
:
'
Forgot
password?'
agreement_prefix
:
'
I
have
read
and
agree
to
the'
agreement_terms
:
'
User
Agreement'
agreement_privacy
:
'
Privacy
Policy'
agreement_and
:
'
and'
confirm_to_logout
:
'
Confirm
to
logout?'
you_can_login_again_after_logout
:
'
You
can
login
again
after
logout'
reset_password_module
:
reset_login_password
:
'
Reset
login
password'
please_enter_your_registered_phone_number
:
"
Please
enter
your
registered
phone
number.
We'll
reset
your
password
for
you"
please_enter_your_new_password
:
'
Please
enter
your
new
password'
back_to_login
:
'
Back
to
login'
reset_password
:
'
Reset
Password'
home_module
:
agent_welcome_message
:
'
Hi,
welcome
to
Model
Link'
...
...
src/locales/langs/zh-cn.yaml
View file @
6d7b65ec
...
...
@@ -43,6 +43,7 @@ common_module:
publish_success_message
:
'
发布成功'
clear_success_message
:
'
清空成功'
add_success_message
:
'
新增成功'
reset_success_message
:
'
重置成功'
loading
:
'
加载中'
updating
:
'
更新中'
successful_update
:
'
更新成功'
...
...
@@ -151,6 +152,8 @@ common_module:
not_certified_yet
:
'
未认证'
authenticated
:
'
已认证'
cancel_authorization
:
'
取消授权'
get_code
:
'
获取验证码'
dialogue_module
:
continue_question_message
:
'
你可以继续提问'
...
...
@@ -210,6 +213,7 @@ router_title_module:
order_manage
:
'
订单管理'
data_statistic
:
'
数据统计'
plugin_center
:
'
插件中心'
reset_password
:
'
重置密码'
login_module
:
app_welcome_words
:
'
Hi,
欢迎使用Model
Link'
...
...
@@ -226,6 +230,25 @@ login_module:
successful
:
'
获取成功'
get_verification_code
:
'
获取验证码'
other_login_methods
:
'
其他登录方式'
mobile_welcome_words
:
'
您好,欢迎使用Model
Link!'
sign_in_with_mobile_number
:
'
推荐使用手机号码登录'
verification_code_login
:
'
验证码登录'
password_login
:
'
密码登录'
email_login
:
'
邮箱登录'
forgot_password
:
'
忘记密码?'
agreement_prefix
:
'
我已阅读并同意'
agreement_terms
:
'
用户协议'
agreement_privacy
:
'
隐私政策'
agreement_and
:
'
与'
confirm_to_logout
:
'
确认要退出登录吗?'
you_can_login_again_after_logout
:
'
退出登录仍可登录此账号'
reset_password_module
:
reset_login_password
:
'
重置登录密码'
please_enter_your_registered_phone_number
:
'
请输入您注册的手机号,我们将为您重置密码'
please_enter_your_new_password
:
'
请输入您的新密码'
back_to_login
:
'
返回登录'
reset_password
:
'
重置密码'
home_module
:
agent_welcome_message
:
'
Hi,
欢迎使用Model
Link'
...
...
src/locales/langs/zh-hk.yaml
View file @
6d7b65ec
...
...
@@ -43,6 +43,7 @@ common_module:
publish_success_message
:
'
發佈成功'
clear_success_message
:
'
清空成功'
add_success_message
:
'
新增成功'
reset_success_message
:
'
重置成功'
loading
:
'
加載中'
updating
:
'
更新中'
successful_update
:
'
更新成功'
...
...
@@ -151,6 +152,7 @@ common_module:
not_certified_yet
:
'
未認證'
authenticated
:
'
已認證'
cancel_authorization
:
'
取消授權'
get_code
:
'
獲取驗証碼'
dialogue_module
:
continue_question_message
:
'
你可以繼續提問'
...
...
@@ -210,6 +212,7 @@ router_title_module:
order_manage
:
'
訂單管理'
data_statistic
:
'
數據統計'
plugin_center
:
'
插件中心'
reset_password
:
'
重置密碼'
login_module
:
app_welcome_words
:
'
Hi,
歡迎使用Model
Link'
...
...
@@ -228,6 +231,25 @@ login_module:
other_login_methods
:
'
其他登錄方式'
interrupt_dialogue_prompt
:
'
當前回復尚未完成,是否確定打斷發起新會話?'
interrupt_the_conversation_and_apply_the_history_prompt
:
'
當前回復尚未完成,是否確定打斷對話應用其它記錄?'
mobile_welcome_words
:
'
您好,歡迎使用Model
Link!'
sign_in_with_mobile_number
:
'
推薦使用手機號碼登錄'
verification_code_login
:
'
驗證碼登錄'
password_login
:
'
密碼登錄'
email_login
:
'
郵箱登錄'
forgot_password
:
'
忘記密碼?'
agreement_prefix
:
'
我已閲讀並同意'
agreement_terms
:
'
用户協議'
agreement_privacy
:
'
隱私政策'
agreement_and
:
'
與'
confirm_to_logout
:
'
確認要退出登錄嗎?'
you_can_login_again_after_logout
:
'
退出登錄仍可登錄此賬號'
reset_password_module
:
reset_login_password
:
'
重置登录密码'
please_enter_your_registered_phone_number
:
'
请输入您注册的手机号,我们将为您重置密码'
please_enter_your_new_password
:
'
请输入您的新密码'
back_to_login
:
'
返回登录'
reset_password
:
'
重置密码'
home_module
:
agent_welcome_message
:
'
Hi,
歡迎使用Model
Link'
...
...
src/router/guards.ts
View file @
6d7b65ec
...
...
@@ -3,7 +3,7 @@ import { useUserStore } from '@/store/modules/user'
import
i18n
from
'@/locales'
/** 路由白名单 */
const
whitePathList
=
[
'/login'
]
const
whitePathList
=
[
'/login'
,
'/reset-password'
]
export
function
createRouterGuards
(
router
:
Router
)
{
router
.
beforeEach
((
to
,
_from
,
next
)
=>
{
...
...
src/router/modules/base.ts
View file @
6d7b65ec
...
...
@@ -8,7 +8,16 @@ export default [
rank
:
1001
,
title
:
'router_title_module.login'
,
},
component
:
()
=>
import
(
'@/views/login/login.vue'
),
component
:
()
=>
import
(
'@/views/login/index.vue'
),
},
{
path
:
'/reset-password'
,
name
:
'ResetPassword'
,
meta
:
{
rank
:
1001
,
title
:
'router_title_module.reset_password'
,
},
component
:
()
=>
import
(
'@/views/reset-password/reset-password.vue'
),
},
// {
// path: '/404',
...
...
src/views/login/h5-login.vue
0 → 100644
View file @
6d7b65ec
<
script
setup
lang=
"ts"
>
import
{
CountdownInst
,
FormInst
,
FormItemRule
,
FormRules
}
from
'naive-ui'
import
isMobilePhone
from
'validator/es/lib/isMobilePhone'
import
{
computed
,
onMounted
,
ref
,
shallowReadonly
,
useTemplateRef
,
watchEffect
}
from
'vue'
import
{
useI18n
}
from
'vue-i18n'
import
SparkMD5
from
'spark-md5'
import
{
Down
,
Mail
,
Lock
,
Phone
}
from
'@icon-park/vue-next'
import
isEmail
from
'validator/es/lib/isEmail'
import
{
useRoute
,
useRouter
}
from
'vue-router'
import
{
ss
}
from
'@/utils/storage'
import
{
fetchEmailCode
,
fetchLogin
,
fetchSMSCode
}
from
'@/apis/user'
import
{
useUserStore
}
from
'@/store/modules/user'
import
{
UserInfo
}
from
'@/store/types/user'
import
{
useSystemLanguageStore
}
from
'@/store/modules/system-language'
type
LoginMethod
=
'password'
|
'sms'
|
'email'
enum
StorageKeyEnum
{
smsCountdownTime
=
'SMS_COUNTDOWN_TIME'
,
emailCountdownTime
=
'MAIL_COUNTDOWN_TIME'
,
}
interface
LoginPayload
{
loginChannel
:
'MEMBER_PLATFOMR_SMS'
|
'MEMBER_PLATFOMR_EMAIL'
|
'MEMBER_PLATFOMR_PW'
account
:
string
password
?:
string
authCode
?:
string
}
const
{
t
}
=
useI18n
()
const
systemLanguageStore
=
useSystemLanguageStore
()
const
userStore
=
useUserStore
()
const
router
=
useRouter
()
const
route
=
useRoute
()
const
currentLoginMethod
=
ref
<
LoginMethod
>
(
'sms'
)
const
currentPhoneNumberArea
=
ref
<
'+86'
|
'+852'
>
(
'+86'
)
const
loginBtnLoading
=
ref
(
false
)
const
passwordLoginFormRef
=
useTemplateRef
<
FormInst
>
(
'passwordLoginFormRef'
)
const
smsLoginFormRef
=
useTemplateRef
<
FormInst
>
(
'smsLoginFormRef'
)
const
emailLoginFormRef
=
useTemplateRef
<
FormInst
>
(
'emailLoginFormRef'
)
const
countdownRef
=
useTemplateRef
<
CountdownInst
>
(
'countdownRef'
)
const
passwordLoginForm
=
ref
({
account
:
''
,
password
:
''
,
})
const
smsLoginForm
=
ref
({
phoneNumber
:
''
,
code
:
''
,
})
const
emailLoginForm
=
ref
({
email
:
''
,
code
:
''
,
})
const
isAgreeTerms
=
ref
(
false
)
const
countdownActive
=
ref
(
true
)
const
isShowCountdown
=
ref
(
false
)
const
countdownDuration
=
ref
<
number
>
(
60000
)
const
passwordLoginFormRules
=
shallowReadonly
<
FormRules
>
({
account
:
{
required
:
true
,
message
:
t
(
'login_module.please_enter_your_account_number'
),
trigger
:
'blur'
},
password
:
{
required
:
true
,
message
:
t
(
'login_module.please_enter_your_password'
),
trigger
:
'blur'
},
})
const
smsLoginFormRules
=
shallowReadonly
<
FormRules
>
({
phoneNumber
:
{
key
:
'phoneNumber'
,
required
:
true
,
validator
:
(
_rule
:
FormItemRule
,
value
:
string
)
=>
{
if
(
!
value
)
{
return
new
Error
(
t
(
'login_module.please_enter_your_cell_phone_number'
))
}
else
if
(
!
isMobilePhone
(
value
,
[
'zh-CN'
,
'zh-HK'
]))
{
return
new
Error
(
t
(
'login_module.please_enter_your_correct_cell_phone_number'
))
}
return
},
trigger
:
'blur'
,
},
code
:
{
required
:
true
,
message
:
t
(
'login_module.please_enter_the_verification_code'
),
trigger
:
'blur'
},
})
const
emailLoginFormRules
=
shallowReadonly
<
FormRules
>
({
email
:
{
key
:
'email'
,
required
:
true
,
validator
:
(
_rule
:
FormItemRule
,
value
:
string
)
=>
{
if
(
!
value
)
{
return
new
Error
(
t
(
'login_module.please_enter_your_email_address'
))
}
else
if
(
!
isEmail
(
value
))
{
return
new
Error
(
t
(
'login_module.please_enter_the_correct_email_address'
))
}
return
},
trigger
:
'blur'
,
},
code
:
{
required
:
true
,
message
:
t
(
'login_module.please_enter_the_verification_code'
),
trigger
:
'blur'
},
})
onMounted
(()
=>
{
document
.
title
=
t
(
'common_module.login'
)
})
const
phoneNumberAreaOptions
=
computed
(()
=>
{
return
[
{
label
:
`+86⠀
${
t
(
'login_module.mainland_china'
)}
`
,
value
:
'+86'
,
},
{
label
:
`+852
${
t
(
'login_module.hong_kong_china'
)}
`
,
value
:
'+852'
,
},
]
})
const
currentLoginMethodValue
=
computed
(()
=>
{
switch
(
currentLoginMethod
.
value
)
{
case
'password'
:
return
t
(
'login_module.password_login'
)
case
'sms'
:
return
t
(
'login_module.verification_code_login'
)
case
'email'
:
return
t
(
'login_module.email_login'
)
default
:
return
''
}
})
const
isEnglishLanguage
=
computed
(()
=>
{
return
systemLanguageStore
.
currentLanguage
===
'en'
})
watchEffect
(()
=>
{
let
timeStringDraft
=
''
switch
(
currentLoginMethod
.
value
)
{
case
'sms'
:
{
timeStringDraft
=
ss
.
get
(
StorageKeyEnum
.
smsCountdownTime
)
}
break
case
'email'
:
{
timeStringDraft
=
ss
.
get
(
StorageKeyEnum
.
emailCountdownTime
)
}
break
}
if
(
timeStringDraft
)
{
const
time
=
Math
.
floor
(
Date
.
now
()
-
parseInt
(
timeStringDraft
))
if
(
time
<
60000
)
{
countdownDuration
.
value
=
60000
-
time
countdownRef
.
value
?.
reset
()
isShowCountdown
.
value
=
true
}
}
})
function
onlyAllowNumber
(
value
:
string
)
{
return
!
value
||
/^
\d
+$/
.
test
(
value
)
}
function
noSideSpace
(
value
:
string
)
{
return
!
value
.
startsWith
(
' '
)
&&
!
value
.
endsWith
(
' '
)
}
function
countdownRender
({
seconds
,
minutes
}:
{
seconds
:
number
;
minutes
:
number
})
{
if
(
minutes
&&
minutes
===
1
)
{
return
'60 s'
}
return
`
${
seconds
}
s`
}
function
onCountdownFinish
()
{
isShowCountdown
.
value
=
false
}
// 获取手机验证码
function
handleSMSCodeGain
()
{
smsLoginFormRef
.
value
?.
validate
(
(
errors
)
=>
{
if
(
errors
)
return
''
countdownDuration
.
value
=
60000
ss
.
set
(
StorageKeyEnum
.
smsCountdownTime
,
Date
.
now
())
countdownRef
.
value
?.
reset
()
isShowCountdown
.
value
=
true
fetchSMSCode
(
getInputPhoneNumber
()).
then
((
res
)
=>
{
if
(
res
.
code
!==
0
)
return
''
window
.
$message
.
success
(
t
(
'login_module.successful'
))
})
},
(
rule
)
=>
{
return
rule
.
key
===
'phoneNumber'
},
)
}
// 获取邮箱验证码
function
handleEmailCodeGain
()
{
emailLoginFormRef
.
value
?.
validate
(
(
errors
)
=>
{
if
(
errors
)
return
''
countdownDuration
.
value
=
60000
ss
.
set
(
StorageKeyEnum
.
emailCountdownTime
,
Date
.
now
())
countdownRef
.
value
?.
reset
()
isShowCountdown
.
value
=
true
fetchEmailCode
(
encodeURIComponent
(
emailLoginForm
.
value
.
email
)).
then
((
res
)
=>
{
if
(
res
.
code
!==
0
)
return
''
window
.
$message
.
success
(
t
(
'login_module.successful'
))
})
},
(
rule
)
=>
{
return
rule
.
key
===
'email'
},
)
}
// 获取输入的手机号码
function
getInputPhoneNumber
()
{
return
currentPhoneNumberArea
.
value
!==
'+86'
?
encodeURIComponent
(
`
${
currentPhoneNumberArea
.
value
}${
smsLoginForm
.
value
.
phoneNumber
}
`
)
:
smsLoginForm
.
value
.
phoneNumber
}
// 忘记密码
function
handleToResetPassword
()
{
const
redirectUrl
=
decodeURIComponent
((
route
.
query
.
redirect
as
string
)
||
''
)
router
.
replace
({
path
:
'/reset-password'
,
query
:
redirectUrl
?
{
redirect
:
encodeURIComponent
(
redirectUrl
)
}
:
{},
})
}
// 切换登录方式
function
handleLoginMethodChange
(
method
:
LoginMethod
)
{
currentLoginMethod
.
value
=
method
isAgreeTerms
.
value
=
false
smsLoginForm
.
value
=
{
phoneNumber
:
''
,
code
:
''
,
}
emailLoginForm
.
value
=
{
email
:
''
,
code
:
''
,
}
passwordLoginForm
.
value
=
{
account
:
''
,
password
:
''
,
}
}
// 登录
function
handleLoginSubmit
(
method
:
LoginMethod
)
{
let
payload
:
LoginPayload
=
{
loginChannel
:
'MEMBER_PLATFOMR_PW'
,
account
:
''
,
password
:
''
,
}
new
Promise
((
resolve
)
=>
{
switch
(
method
)
{
case
'password'
:
{
passwordLoginFormRef
.
value
?.
validate
((
errors
)
=>
{
if
(
errors
)
return
''
payload
=
{
loginChannel
:
'MEMBER_PLATFOMR_PW'
,
account
:
passwordLoginForm
.
value
.
account
,
password
:
SparkMD5
.
hash
(
passwordLoginForm
.
value
.
password
),
}
resolve
(
true
)
})
}
break
case
'sms'
:
{
smsLoginFormRef
.
value
?.
validate
((
errors
)
=>
{
if
(
errors
)
return
''
payload
=
{
loginChannel
:
'MEMBER_PLATFOMR_SMS'
,
account
:
smsLoginForm
.
value
.
phoneNumber
,
authCode
:
smsLoginForm
.
value
.
code
,
}
resolve
(
true
)
})
}
break
case
'email'
:
{
emailLoginFormRef
.
value
?.
validate
((
errors
)
=>
{
if
(
errors
)
return
''
payload
=
{
loginChannel
:
'MEMBER_PLATFOMR_EMAIL'
,
account
:
emailLoginForm
.
value
.
email
,
authCode
:
emailLoginForm
.
value
.
code
,
}
resolve
(
true
)
})
}
break
}
}).
then
(()
=>
{
loginBtnLoading
.
value
=
true
fetchLogin
<
UserInfo
&
{
token
:
string
}
>
(
payload
)
.
then
((
res
)
=>
{
if
(
res
.
code
!==
0
)
return
''
userStore
.
updateToken
(
res
.
data
.
token
)
userStore
.
updateUserInfo
({
avatarUrl
:
res
.
data
.
avatarUrl
,
memberId
:
res
.
data
.
memberId
,
mobilePhone
:
res
.
data
.
mobilePhone
,
nickName
:
res
.
data
.
nickName
,
remark
:
res
.
data
.
remark
,
email
:
res
.
data
.
email
,
})
const
redirectUrl
=
decodeURIComponent
((
route
.
query
.
redirect
as
string
)
||
''
)
router
.
replace
({
path
:
redirectUrl
?
redirectUrl
:
'/'
})
window
.
$message
.
success
(
t
(
'login_module.login_success'
))
ss
.
remove
(
StorageKeyEnum
.
smsCountdownTime
)
ss
.
remove
(
StorageKeyEnum
.
emailCountdownTime
)
})
.
finally
(()
=>
{
loginBtnLoading
.
value
=
false
})
})
}
</
script
>
<
template
>
<div
class=
"bg-px-login-h5_login_bg-png relative flex h-full w-full flex-col justify-between bg-cover bg-center bg-no-repeat py-[44px]"
>
<div
class=
"px-[33px]"
>
<p
class=
"font-family-medium text-[18px]"
>
{{
t
(
'login_module.mobile_welcome_words'
)
}}
</p>
<p
class=
"text-gray-font-color mt-[9px] text-[12px]"
>
{{
t
(
'login_module.sign_in_with_mobile_number'
)
}}
</p>
<div
class=
"text-theme-color border-b-theme-color font-family-medium my-[24px] inline-block text-[13px]"
>
<span>
{{
currentLoginMethodValue
}}
</span>
<div
class=
"bg-theme-color h-[2px] w-full rounded-[1px]"
/>
</div>
<!-- SMS登录 -->
<n-form
v-if=
"currentLoginMethod === 'sms'"
ref=
"smsLoginFormRef"
label-placement=
"left"
size=
"large"
:model=
"smsLoginForm"
:rules=
"smsLoginFormRules"
>
<n-form-item
path=
"phoneNumber"
feedback-class=
"text-[12px]!"
>
<n-input
v-model:value
.
trim=
"smsLoginForm.phoneNumber"
:allow-input=
"onlyAllowNumber"
:maxlength=
"currentPhoneNumberArea === '+852' ? 8 : 11"
:placeholder=
"t('login_module.please_enter_your_cell_phone_number')"
>
<template
#
prefix
>
<div
class=
"flex items-center"
>
<n-popselect
v-model:value=
"currentPhoneNumberArea"
:options=
"phoneNumberAreaOptions"
trigger=
"click"
>
<div
class=
"flex w-[54px] cursor-pointer items-center"
>
<div
class=
"mr-[4px] text-[14px]"
>
{{
currentPhoneNumberArea
}}
</div>
<Down
theme=
"outline"
size=
"18"
fill=
"#333"
:stroke-width=
"3"
class=
"mt-[2px]"
/>
</div>
</n-popselect>
<div
class=
"mx-[8px] h-[18px] w-[1px] bg-[#868686]"
></div>
</div>
</
template
>
</n-input>
</n-form-item>
<n-form-item
path=
"code"
feedback-class=
"text-[12px]!"
>
<n-input
v-model:value=
"smsLoginForm.code"
:allow-input=
"onlyAllowNumber"
show-password-on=
"click"
:maxlength=
"6"
:placeholder=
"t('login_module.please_enter_the_verification_code')"
>
</n-input>
<n-button
v-show=
"!isShowCountdown"
class=
"text-[11px]! ml-[9px]! text-[#333]! px-[10px]! bg-white!"
:class=
"isEnglishLanguage ? 'w-[70px]!' : 'w-[90px]!'"
:disabled=
"!smsLoginForm.phoneNumber"
@
click=
"handleSMSCodeGain"
>
<span>
{{ t('common_module.get_code') }}
</span>
</n-button>
<div
v-show=
"isShowCountdown"
class=
"flex-center rounded-theme ml-[9px]! text-gray-font-color h-[40px] flex-shrink-0 border bg-white text-center text-[12px]"
:class=
"isEnglishLanguage ? 'w-[70px]' : 'w-[90px]'"
>
<n-countdown
ref=
"countdownRef"
:duration=
"countdownDuration"
:active=
"countdownActive"
:render=
"countdownRender"
:on-finish=
"onCountdownFinish"
/>
</div>
</n-form-item>
<n-form-item
class=
"mt-4"
>
<n-button
type=
"primary"
size=
"large"
block
:loading=
"loginBtnLoading"
:disabled=
"!smsLoginForm.phoneNumber || !smsLoginForm.code || !isAgreeTerms"
@
click=
"handleLoginSubmit('sms')"
>
{{ t('common_module.login') }}
</n-button>
</n-form-item>
</n-form>
<!-- 密码登录 -->
<n-form
v-if=
"currentLoginMethod === 'password'"
ref=
"passwordLoginFormRef"
label-placement=
"left"
size=
"large"
:model=
"passwordLoginForm"
:rules=
"passwordLoginFormRules"
feedback-class=
"text-[12px]!"
>
<n-form-item
path=
"account"
feedback-class=
"text-[12px]!"
>
<n-input
v-model:value=
"passwordLoginForm.account"
:allow-input=
"noSideSpace"
:maxlength=
"50"
:placeholder=
"t('login_module.please_enter_your_account_number')"
/>
</n-form-item>
<n-form-item
path=
"password"
feedback-class=
"text-[12px]!"
>
<n-input
v-model:value=
"passwordLoginForm.password"
class=
"font-sans"
:allow-input=
"noSideSpace"
type=
"password"
show-password-on=
"click"
:placeholder=
"t('login_module.please_enter_your_password')"
/>
</n-form-item>
<div
class=
"flex w-full justify-end"
>
<span
class=
"text-[11px] text-[#518AFF]"
@
click=
"handleToResetPassword"
>
{{ t('login_module.forgot_password') }}
</span>
</div>
<n-form-item
class=
"mt-4"
>
<n-button
type=
"primary"
size=
"large"
block
:loading=
"loginBtnLoading"
:disabled=
"!passwordLoginForm.account || !passwordLoginForm.password || !isAgreeTerms"
@
click=
"handleLoginSubmit('password')"
>
{{ t('common_module.login') }}
</n-button>
</n-form-item>
</n-form>
<!-- 邮箱登录 -->
<n-form
v-if=
"currentLoginMethod === 'email'"
ref=
"emailLoginFormRef"
label-placement=
"left"
size=
"large"
:model=
"emailLoginForm"
:rules=
"emailLoginFormRules"
>
<n-form-item
path=
"email"
feedback-class=
"text-[12px]!"
>
<n-input
v-model:value=
"emailLoginForm.email"
:allow-input=
"noSideSpace"
:placeholder=
"t('login_module.please_enter_your_email_address')"
:maxlength=
"50"
>
</n-input>
</n-form-item>
<n-form-item
path=
"code"
feedback-class=
"text-[12px]!"
>
<n-input
v-model:value=
"emailLoginForm.code"
:allow-input=
"onlyAllowNumber"
show-password-on=
"click"
:maxlength=
"6"
:placeholder=
"t('login_module.please_enter_the_verification_code')"
>
</n-input>
<n-button
v-show=
"!isShowCountdown"
class=
"text-[11px]! ml-[9px]! text-[#333]! px-[10px]! bg-white!"
:class=
"isEnglishLanguage ? 'w-[70px]!' : 'w-[90px]!'"
:disabled=
"!emailLoginForm.email"
@
click=
"handleEmailCodeGain"
>
<span>
{{ t('common_module.get_code') }}
</span>
</n-button>
<div
v-show=
"isShowCountdown"
class=
"flex-center rounded-theme ml-[9px]! text-gray-font-color h-[40px] flex-shrink-0 border bg-white text-center text-[12px]"
:class=
"isEnglishLanguage ? 'w-[70px]' : 'w-[90px]'"
>
<n-countdown
ref=
"countdownRef"
:duration=
"countdownDuration"
:active=
"countdownActive"
:render=
"countdownRender"
:on-finish=
"onCountdownFinish"
/>
</div>
</n-form-item>
<n-form-item
class=
"mt-4"
>
<n-button
type=
"primary"
size=
"large"
block
:loading=
"loginBtnLoading"
:disabled=
"!emailLoginForm.email || !emailLoginForm.code || !isAgreeTerms"
@
click=
"handleLoginSubmit('email')"
>
{{ t('common_module.login') }}
</n-button>
</n-form-item>
</n-form>
<div
class=
"flex-center gap-[7px] text-[11px]"
>
<n-checkbox
v-model:checked=
"isAgreeTerms"
class=
"h-[12px] w-[12px]"
/>
<div>
{{ t('login_module.agreement_prefix') }}
<span
class=
"text-[#518AFF]"
>
{{ t('login_module.agreement_terms') }}
</span>
{{ t('login_module.agreement_and') }}
<span
class=
"text-[#518AFF]"
>
{{ t('login_module.agreement_privacy') }}
</span>
</div>
</div>
</div>
<div
class=
"absolute top-[450px] w-full px-[33px]"
>
<div
class=
"mb-[44px] flex flex-col items-center"
>
<n-divider
class=
"w-[70%]!"
>
<span
class=
"text-gray-font-color text-[11px]"
>
{{ t('login_module.other_login_methods') }}
</span>
</n-divider>
<div
class=
"flex-center gap-[15px]"
>
<div
v-show=
"currentLoginMethod !== 'password'"
class=
"flex-center h-[34px] w-[34px] cursor-pointer rounded-full bg-[#F2F2F2]"
@
click=
"handleLoginMethodChange('password')"
>
<Lock
theme=
"outline"
size=
"16"
fill=
"#000DFF"
:stroke-width=
"3"
/>
</div>
<div
v-show=
"currentLoginMethod !== 'email'"
class=
"flex-center h-[34px] w-[34px] cursor-pointer rounded-full bg-[#F2F2F2]"
@
click=
"handleLoginMethodChange('email')"
>
<Mail
theme=
"outline"
size=
"16"
fill=
"#000DFF"
:stroke-width=
"3"
/>
</div>
<div
v-show=
"currentLoginMethod !== 'sms'"
class=
"flex-center h-[34px] w-[34px] cursor-pointer rounded-full bg-[#F2F2F2]"
@
click=
"handleLoginMethodChange('sms')"
>
<Phone
theme=
"outline"
size=
"16"
fill=
"#000DFF"
:stroke-width=
"3"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<
style
lang=
"scss"
scoped
>
:deep
(
.n-checkbox-box-wrapper
),
:deep
(
.n-checkbox-box
)
{
width
:
12px
!
important
;
height
:
12px
!
important
;
border-radius
:
100%
!
important
;
}
:deep
(
.--n-feedback-height
)
{
height
:
16px
!
important
;
}
:deep
(
.n-input__placeholder
)
{
font-size
:
13px
!
important
;
}
</
style
>
src/views/login/index.vue
0 → 100644
View file @
6d7b65ec
<
script
setup
lang=
"ts"
>
import
{
useLayoutConfig
}
from
'@/composables/useLayoutConfig'
import
H5Login
from
'./h5-login.vue'
import
PCLogin
from
'./login.vue'
const
{
isMobile
}
=
useLayoutConfig
()
</
script
>
<
template
>
<component
:is=
"isMobile ? H5Login : PCLogin"
></component>
</
template
>
src/views/reset-password/reset-password.vue
0 → 100644
View file @
6d7b65ec
<
script
setup
lang=
"ts"
>
import
{
CountdownInst
,
FormInst
,
FormItemRule
,
FormRules
}
from
'naive-ui'
import
isMobilePhone
from
'validator/es/lib/isMobilePhone'
import
{
computed
,
ref
,
shallowReadonly
,
useTemplateRef
}
from
'vue'
import
{
useI18n
}
from
'vue-i18n'
import
{
Down
}
from
'@icon-park/vue-next'
import
{
useRoute
,
useRouter
}
from
'vue-router'
import
SparkMD5
from
'spark-md5'
import
{
fetchForgetMemberPassword
,
fetchSMSCode
,
fetchVerifyCode
}
from
'@/apis/user'
import
{
ss
}
from
'@/utils/storage'
import
{
useSystemLanguageStore
}
from
'@/store/modules/system-language'
interface
PasswordInfoFormInterface
{
authCode
:
string
password
:
string
confirmPassword
:
string
}
const
{
t
}
=
useI18n
()
const
systemLanguageStore
=
useSystemLanguageStore
()
const
route
=
useRoute
()
const
router
=
useRouter
()
const
smsLoginFormRef
=
useTemplateRef
<
FormInst
>
(
'smsLoginFormRef'
)
const
passwordInfoFormRef
=
useTemplateRef
<
FormInst
>
(
'passwordInfoFormRef'
)
const
countdownRef
=
useTemplateRef
<
CountdownInst
>
(
'countdownRef'
)
const
isFinishAuthCode
=
ref
(
false
)
const
submitBtnLoading
=
ref
(
false
)
const
countdownActive
=
ref
(
true
)
const
isShowCountdown
=
ref
(
false
)
const
countdownDuration
=
ref
<
number
>
(
60000
)
const
currentPhoneNumberArea
=
ref
<
'+86'
|
'+852'
>
(
'+86'
)
const
smsLoginForm
=
ref
({
phoneNumber
:
''
,
code
:
''
,
})
const
passwordInfoForm
=
ref
<
PasswordInfoFormInterface
>
({
authCode
:
''
,
password
:
''
,
confirmPassword
:
''
,
})
const
smsLoginFormRules
=
shallowReadonly
<
FormRules
>
({
phoneNumber
:
{
key
:
'phoneNumber'
,
required
:
true
,
validator
:
(
_rule
:
FormItemRule
,
value
:
string
)
=>
{
if
(
!
value
)
{
return
new
Error
(
t
(
'login_module.please_enter_your_cell_phone_number'
))
}
else
if
(
!
isMobilePhone
(
value
,
[
'zh-CN'
,
'zh-HK'
]))
{
return
new
Error
(
t
(
'login_module.please_enter_your_correct_cell_phone_number'
))
}
return
},
trigger
:
'blur'
,
},
code
:
{
required
:
true
,
message
:
t
(
'login_module.please_enter_the_verification_code'
),
trigger
:
'blur'
},
})
const
passwordFormRules
=
shallowReadonly
({
password
:
{
required
:
true
,
trigger
:
'blur'
,
validator
:
(
_rule
:
FormItemRule
,
value
:
string
)
=>
{
if
(
!
value
)
{
return
new
Error
(
t
(
'personal_settings_module.please_enter_your_new_password'
))
}
else
if
(
value
.
length
<
6
)
{
return
new
Error
(
t
(
'personal_settings_module.the_password_contains_a_maximum_of_6_characters'
))
}
return
},
},
confirmPassword
:
{
required
:
true
,
trigger
:
'blur'
,
validator
:
(
_rule
:
FormItemRule
,
value
:
string
)
=>
{
if
(
!
value
)
{
return
new
Error
(
t
(
'personal_settings_module.please_enter_confirm_new_password'
))
}
else
if
(
value
!==
passwordInfoForm
.
value
.
password
)
{
return
new
Error
(
t
(
'personal_settings_module.verify_that_the_new_password_is_inconsistent_with_the_new_password'
),
)
}
return
},
},
})
const
phoneNumberAreaOptions
=
computed
(()
=>
{
return
[
{
label
:
`+86⠀
${
t
(
'login_module.mainland_china'
)}
`
,
value
:
'+86'
,
},
{
label
:
`+852
${
t
(
'login_module.hong_kong_china'
)}
`
,
value
:
'+852'
,
},
]
})
const
isEnglishLanguage
=
computed
(()
=>
{
return
systemLanguageStore
.
currentLanguage
===
'en'
})
function
noSideSpace
(
value
:
string
)
{
return
!
value
.
startsWith
(
' '
)
&&
!
value
.
endsWith
(
' '
)
}
function
onlyAllowNumber
(
value
:
string
)
{
return
!
value
||
/^
\d
+$/
.
test
(
value
)
}
function
countdownRender
({
seconds
,
minutes
}:
{
seconds
:
number
;
minutes
:
number
})
{
if
(
minutes
&&
minutes
===
1
)
{
return
'60 s'
}
return
`
${
seconds
}
s`
}
function
onCountdownFinish
()
{
isShowCountdown
.
value
=
false
}
function
handleSMSCodeGain
()
{
smsLoginFormRef
.
value
?.
validate
(
(
errors
)
=>
{
if
(
errors
)
return
''
countdownDuration
.
value
=
60000
ss
.
set
(
'SMS_COUNTDOWN_TIME'
,
Date
.
now
())
countdownRef
.
value
?.
reset
()
isShowCountdown
.
value
=
true
fetchSMSCode
(
getInputPhoneNumber
()).
then
((
res
)
=>
{
if
(
res
.
code
!==
0
)
return
''
window
.
$message
.
success
(
t
(
'login_module.successful'
))
})
},
(
rule
)
=>
{
return
rule
.
key
===
'phoneNumber'
},
)
}
function
getInputPhoneNumber
()
{
return
currentPhoneNumberArea
.
value
!==
'+86'
?
encodeURIComponent
(
`
${
currentPhoneNumberArea
.
value
}${
smsLoginForm
.
value
.
phoneNumber
}
`
)
:
smsLoginForm
.
value
.
phoneNumber
}
function
handleBackLogin
()
{
const
redirectUrl
=
decodeURIComponent
((
route
.
query
.
redirect
as
string
)
||
''
)
router
.
replace
({
path
:
'/login'
,
query
:
redirectUrl
?
{
redirect
:
encodeURIComponent
(
redirectUrl
)
}
:
{}
})
}
function
handleNextStep
()
{
smsLoginFormRef
.
value
?.
validate
((
errors
)
=>
{
if
(
errors
)
return
''
submitBtnLoading
.
value
=
true
fetchVerifyCode
<
string
>
(
smsLoginForm
.
value
.
phoneNumber
,
smsLoginForm
.
value
.
code
)
.
then
((
res
)
=>
{
if
(
res
.
code
===
0
)
{
isShowCountdown
.
value
=
false
isFinishAuthCode
.
value
=
true
passwordInfoForm
.
value
.
authCode
=
res
.
data
}
})
.
finally
(()
=>
{
submitBtnLoading
.
value
=
false
})
})
}
function
handleResetPassword
()
{
passwordInfoFormRef
.
value
?.
validate
((
errors
)
=>
{
if
(
errors
)
return
''
submitBtnLoading
.
value
=
true
fetchForgetMemberPassword
(
smsLoginForm
.
value
.
phoneNumber
,
passwordInfoForm
.
value
.
authCode
,
SparkMD5
.
hash
(
passwordInfoForm
.
value
.
confirmPassword
),
)
.
then
((
res
)
=>
{
if
(
res
.
code
===
0
)
{
window
.
$message
.
success
(
t
(
'common_module.reset_success_message'
))
handleBackLogin
()
}
})
.
finally
(()
=>
{
submitBtnLoading
.
value
=
false
})
})
}
</
script
>
<
template
>
<div
class=
"bg-px-login-h5_login_bg-png relative flex h-full w-full flex-col justify-between bg-cover bg-center bg-no-repeat py-[44px]"
>
<div
class=
"px-[33px]"
>
<p
class=
"font-family-medium text-[18px]"
>
{{
t
(
'reset_password_module.reset_login_password'
)
}}
</p>
<p
class=
"text-gray-font-color mt-[9px] text-[12px]"
>
{{
isFinishAuthCode
?
t
(
'reset_password_module.please_enter_your_new_password'
)
:
t
(
'reset_password_module.please_enter_your_registered_phone_number'
)
}}
</p>
<!-- 验证码校验 -->
<n-form
v-if=
"!isFinishAuthCode"
ref=
"smsLoginFormRef"
label-placement=
"left"
size=
"large"
:model=
"smsLoginForm"
:rules=
"smsLoginFormRules"
class=
"mt-[30px]"
>
<n-form-item
path=
"phoneNumber"
feedback-class=
"text-[12px]!"
>
<n-input
v-model:value
.
trim=
"smsLoginForm.phoneNumber"
:allow-input=
"onlyAllowNumber"
:maxlength=
"currentPhoneNumberArea === '+852' ? 8 : 11"
:placeholder=
"t('login_module.please_enter_your_cell_phone_number')"
>
<template
#
prefix
>
<div
class=
"flex items-center"
>
<n-popselect
v-model:value=
"currentPhoneNumberArea"
:options=
"phoneNumberAreaOptions"
trigger=
"click"
>
<div
class=
"flex w-[54px] cursor-pointer items-center"
>
<div
class=
"mr-[4px] text-[14px]"
>
{{
currentPhoneNumberArea
}}
</div>
<Down
theme=
"outline"
size=
"18"
fill=
"#333"
:stroke-width=
"3"
class=
"mt-[2px]"
/>
</div>
</n-popselect>
<div
class=
"mx-[8px] h-[18px] w-[1px] bg-[#868686]"
></div>
</div>
</
template
>
</n-input>
</n-form-item>
<n-form-item
path=
"code"
feedback-class=
"text-[12px]!"
>
<n-input
v-model:value=
"smsLoginForm.code"
:allow-input=
"onlyAllowNumber"
show-password-on=
"click"
:maxlength=
"6"
:placeholder=
"t('login_module.please_enter_the_verification_code')"
>
</n-input>
<n-button
v-show=
"!isShowCountdown"
class=
"text-[11px]! ml-[9px]! text-[#333]! px-[10px]! bg-white!"
:class=
"isEnglishLanguage ? 'w-[70px]!' : 'w-[90px]!'"
:disabled=
"!smsLoginForm.phoneNumber"
@
click=
"handleSMSCodeGain"
>
<span>
{{ t('common_module.get_code') }}
</span>
</n-button>
<div
v-show=
"isShowCountdown"
class=
"flex-center rounded-theme ml-[9px]! text-gray-font-color h-[40px] flex-shrink-0 border bg-white text-center text-[12px]"
:class=
"isEnglishLanguage ? 'w-[70px]' : 'w-[90px]'"
>
<n-countdown
ref=
"countdownRef"
:duration=
"countdownDuration"
:active=
"countdownActive"
:render=
"countdownRender"
:on-finish=
"onCountdownFinish"
/>
</div>
</n-form-item>
<n-form-item
class=
"mt-4"
>
<n-button
type=
"primary"
size=
"large"
block
:loading=
"submitBtnLoading"
:disabled=
"!smsLoginForm.phoneNumber || !smsLoginForm.code"
@
click=
"handleNextStep"
>
{{ t('common_module.next_btn_text') }}
</n-button>
</n-form-item>
</n-form>
<!-- 重置密码 -->
<n-form
v-if=
"isFinishAuthCode"
ref=
"passwordInfoFormRef"
label-placement=
"left"
size=
"large"
:model=
"passwordInfoForm"
:rules=
"passwordFormRules"
class=
"mt-[30px]"
>
<n-form-item
path=
"password"
>
<n-input
v-model:value=
"passwordInfoForm.password"
type=
"password"
show-password-on=
"click"
class=
"font-sans"
:allow-input=
"noSideSpace"
:placeholder=
"t('personal_settings_module.please_enter_your_new_password')"
/>
</n-form-item>
<n-form-item
path=
"confirmPassword"
>
<n-input
v-model:value=
"passwordInfoForm.confirmPassword"
type=
"password"
show-password-on=
"click"
class=
"font-sans"
:allow-input=
"noSideSpace"
:placeholder=
"t('personal_settings_module.please_enter_confirm_new_password')"
/>
</n-form-item>
<n-form-item
class=
"mt-4"
>
<n-button
type=
"primary"
size=
"large"
block
:loading=
"submitBtnLoading"
:disabled=
"!passwordInfoForm.password || !passwordInfoForm.confirmPassword"
@
click=
"handleResetPassword"
>
{{ t('reset_password_module.reset_password') }}
</n-button>
</n-form-item>
</n-form>
</div>
<div
class=
"flex-center absolute top-[500px] w-full"
>
<div
class=
"inline-block cursor-pointer border-b border-[#518AFF] text-[12px] leading-[14px] text-[#518AFF]"
@
click=
"handleBackLogin"
>
{{ t('reset_password_module.back_to_login') }}
</div>
</div>
</div>
</template>
<
style
lang=
"scss"
scoped
>
:deep
(
.--n-feedback-height
)
{
height
:
16px
!
important
;
}
:deep
(
.n-input__placeholder
)
{
font-size
:
13px
!
important
;
}
</
style
>
src/views/share/components/custom-loading.vue
View file @
6d7b65ec
<
script
setup
lang=
"ts"
></
script
>
<
script
setup
lang=
"ts"
>
interface
Props
{
activeColor
?:
string
}
const
{
activeColor
=
'#000dff'
}
=
defineProps
<
Props
>
()
</
script
>
<
template
>
<div
class=
"loader"
/>
...
...
@@ -14,16 +20,16 @@
@keyframes
l5
{
0
%
{
background
:
#000dff
;
background
:
v-bind
(
'activeColor'
)
;
box-shadow
:
13px
0
#000dff
,
13px
0
v-bind
(
'activeColor'
)
,
-13px
0
#0002
;
}
33
%
{
background
:
#0002
;
box-shadow
:
13px
0
#000dff
,
13px
0
v-bind
(
'activeColor'
)
,
-13px
0
#0002
;
}
...
...
@@ -31,14 +37,14 @@
background
:
#0002
;
box-shadow
:
13px
0
#0002
,
-13px
0
#000dff
;
-13px
0
v-bind
(
'activeColor'
)
;
}
100
%
{
background
:
#000dff
;
background
:
v-bind
(
'activeColor'
)
;
box-shadow
:
13px
0
#0002
,
-13px
0
#000dff
;
-13px
0
v-bind
(
'activeColor'
)
;
}
}
</
style
>
src/views/share/components/mobile-page-header.vue
deleted
100644 → 0
View file @
2bb401e2
<
script
setup
lang=
"ts"
>
import
{
useUserStore
}
from
'@/store/modules/user'
import
{
computed
}
from
'vue'
import
{
useI18n
}
from
'vue-i18n'
interface
Props
{
agentTitle
:
string
}
const
{
t
}
=
useI18n
()
defineProps
<
Props
>
()
const
emit
=
defineEmits
<
{
toCreateApplication
:
[]
toLogin
:
[]
}
>
()
const
userStore
=
useUserStore
()
const
isLogin
=
computed
(()
=>
{
return
userStore
.
isLogin
})
function
handleToCreateApplication
()
{
emit
(
'toCreateApplication'
)
}
function
handleToLogin
()
{
emit
(
'toLogin'
)
}
</
script
>
<
template
>
<header
class=
"flex h-[48px] w-full items-center justify-between border-b border-[#e8e9eb] bg-white px-4"
>
<div
class=
"bg-px-logo-png bg-contain! h-[24px] w-[100px] bg-center bg-no-repeat"
/>
<div>
<NButton
v-show=
"isLogin"
type=
"primary"
class=
"rounded-md! h-[32px]! text-xs! min-w-[80px]!"
@
click=
"handleToCreateApplication"
>
{{
t
(
'common_module.create_agent_btn_text'
)
}}
</NButton>
<NButton
v-show=
"!isLogin"
type=
"primary"
class=
"rounded-md! h-[32px]! text-xs! w-[80px]!"
@
click=
"handleToLogin"
>
<span
class=
"text-xs"
>
{{
t
(
'common_module.login_now'
)
}}
</span>
</NButton>
</div>
</header>
</
template
>
src/views/share/components/mobile/continue-question.vue
0 → 100644
View file @
6d7b65ec
<
script
setup
lang=
"ts"
>
import
{
debounce
}
from
'lodash-es'
import
{
nanoid
}
from
'nanoid'
import
{
computed
,
inject
,
ref
}
from
'vue'
import
{
useI18n
}
from
'vue-i18n'
import
{
Emitter
}
from
'mitt'
import
{
fetchRecommendQuestionList
}
from
'@/apis/home-agent'
interface
Props
{
type
:
'continuous'
|
'featured'
}
defineProps
<
Props
>
()
const
{
t
}
=
useI18n
()
const
emitter
=
inject
<
Emitter
<
MittEvents
>>
(
'emitter'
)
const
continuousQuestionList
=
defineModel
<
string
[]
>
(
'continuousQuestionList'
,
{
required
:
true
})
const
promptChangeBtnStatus
=
ref
<
'normal'
|
'loading'
|
'failed'
>
(
'normal'
)
const
recommendQuestionList
=
computed
(()
=>
{
if
(
continuousQuestionList
.
value
.
length
===
0
)
{
return
[]
}
return
continuousQuestionList
.
value
.
map
((
item
)
=>
{
return
{
id
:
nanoid
(),
content
:
item
,
}
})
})
function
getRecommendQuestionList
()
{
return
fetchRecommendQuestionList
<
string
[]
>
().
then
((
res
)
=>
{
continuousQuestionList
.
value
=
res
.
data
})
}
const
handleRecommendQuestionListUpdate
=
debounce
(
()
=>
{
if
(
promptChangeBtnStatus
.
value
===
'loading'
)
{
return
}
promptChangeBtnStatus
.
value
=
'loading'
getRecommendQuestionList
()
.
then
(()
=>
{
promptChangeBtnStatus
.
value
=
'normal'
})
.
catch
(()
=>
{
promptChangeBtnStatus
.
value
=
'failed'
})
},
700
,
{
leading
:
true
,
trailing
:
false
},
)
function
handleSelectContinueQuestion
(
continueQuestion
:
string
)
{
emitter
?.
emit
(
'selectQuestion'
,
continueQuestion
)
}
</
script
>
<
template
>
<h3
class=
"mt-[15px] text-[12px] text-[#999]"
>
{{
type
===
'featured'
?
t
(
'common_module.recommended_questions'
)
+
':'
:
t
(
'common_module.dialogue_module.continue_question_message'
)
}}
</h3>
<ul
class=
"w-full select-none"
>
<template
v-if=
"continuousQuestionList.length && promptChangeBtnStatus !== 'loading'"
>
<li
v-for=
"questionItem in recommendQuestionList"
:key=
"questionItem.id"
class=
"mt-[10px] w-fit cursor-pointer rounded-[10px] border border-[#d4d6d9] bg-[#ffffff80] px-[12px] py-[10px] text-[12px] hover:opacity-80"
@
click=
"handleSelectContinueQuestion(questionItem.content)"
>
{{
questionItem
.
content
}}
</li>
</
template
>
<
template
v-else
>
<n-skeleton
:class=
"'mt-[10px]'"
height=
"41px"
width=
"52%"
round
/>
<n-skeleton
:class=
"'mt-[10px]'"
height=
"41px"
width=
"72%"
round
/>
<n-skeleton
:class=
"'mt-[10px]'"
height=
"41px"
width=
"70%"
round
/>
</
template
>
<li
class=
"mt-[10px] pl-[16px] text-[12px]"
>
<span
class=
"group cursor-pointer text-[#0B7DFF] transition"
:class=
"{
'hover:text-[#096EE0]': promptChangeBtnStatus !== 'loading',
'cursor-not-allowed': promptChangeBtnStatus === 'loading',
'text-[#BDBDBD]': promptChangeBtnStatus === 'loading',
}"
@
click=
"handleRecommendQuestionListUpdate"
>
<i
class=
"iconfont icon-huanyihuan mr-[2px] inline-block text-[11px] transition-[rotate] duration-150 ease-in-out"
:class=
"{ 'group-active:rotate-360': promptChangeBtnStatus !== 'loading' }"
></i>
<span>
{{ promptChangeBtnStatus === 'failed' ? t('common_module.retry') : t('common_module.exchange') }}
</span>
</span>
</li>
</ul>
</template>
src/views/share/components/mobile/footer-input.vue
0 → 100644
View file @
6d7b65ec
<
script
setup
lang=
"ts"
>
import
{
computed
,
inject
,
onMounted
,
onUnmounted
,
ref
,
watch
}
from
'vue'
import
{
Emitter
}
from
'mitt'
import
{
useI18n
}
from
'vue-i18n'
import
{
nanoid
}
from
'nanoid'
import
{
useRoute
}
from
'vue-router'
import
{
CloseSmall
}
from
'@icon-park/vue-next'
import
{
fetchCustomEventSource
}
from
'@/composables/useEventSource'
import
{
useUserStore
}
from
'@/store/modules/user'
import
{
UploadStatus
}
from
'@/enums/upload-status'
import
{
useDialogueFile
}
from
'@/composables/useDialogueFile'
import
{
useLayoutConfig
}
from
'@/composables/useLayoutConfig'
import
{
TEXTTOSPEECH_WS_URL
}
from
'@/config/base-url'
import
WebSocketCtr
from
'@/utils/web-socket-ctr'
import
{
ChannelType
}
from
'@/enums/channel'
import
{
useUploadImage
}
from
'@/composables/useUploadImage'
import
{
showDialog
}
from
'vant'
interface
Props
{
agentId
:
string
dialogsId
:
string
messageList
:
Map
<
string
,
ConversationMessageItem
>
continuousQuestionStatus
:
'default'
|
'close'
isEnableDocumentParse
:
boolean
isEnableUploadImage
:
boolean
isEnableVoice
:
boolean
answerAudioAutoPlay
:
boolean
answerAudioPlaying
:
boolean
timbreId
:
string
}
const
{
t
}
=
useI18n
()
const
{
query
}
=
useRoute
()
const
props
=
defineProps
<
Props
>
()
const
emit
=
defineEmits
<
{
addMessageItem
:
[
messageId
:
string
,
value
:
ConversationMessageItem
]
updateSpecifyMessageItem
:
[
messageId
:
string
,
newObj
:
Partial
<
ConversationMessageItem
>
]
deleteMessageItem
:
[
messageId
:
string
]
updatePageScroll
:
[]
clearAllMessage
:
[]
toLogin
:
[]
createContinueQuestions
:
[
value
:
string
]
resetContinueQuestionList
:
[]
audioPlay
:
[
messageItem
:
ConversationMessageItem
,
requestId
?:
string
]
audioPause
:
[]
}
>
()
const
{
isMobile
}
=
useLayoutConfig
()
const
userStore
=
useUserStore
()
const
{
uploadFileList
,
handleLimitUpload
,
handleUpload
,
handleRemoveFile
}
=
useDialogueFile
()
const
{
uploadImageList
,
handleLimitUploadImage
,
handleUploadImage
,
handleRemoveUploadImage
}
=
useUploadImage
()
const
isAnswerResponseWait
=
defineModel
<
boolean
>
(
'isAnswerResponseLoading'
,
{
required
:
true
})
const
isAnswerResponseInterrupt
=
defineModel
<
boolean
>
(
'isAnswerResponseInterrupt'
,
{
required
:
true
})
const
emitter
=
inject
<
Emitter
<
MittEvents
>>
(
'emitter'
)
const
inputMessageContent
=
ref
(
''
)
const
currentReplyContentSentenceExtractIndex
=
ref
(
0
)
const
sentenceFragmentSerialNo
=
ref
(
0
)
const
sentenceExtractCheckEnabled
=
ref
(
false
)
const
assistantFullAnswerContent
=
ref
(
''
)
const
sentenceSpeechException
=
ref
(
false
)
const
messageAudioLoading
=
ref
(
false
)
const
currentLatestMessageItemKeyMap
=
ref
(
new
Map
<
'assistant'
|
'user'
,
string
>
())
let
controller
:
AbortController
|
null
=
null
const
isLogin
=
computed
(()
=>
{
return
userStore
.
isLogin
})
const
isAllowClearMessage
=
computed
(()
=>
{
return
props
.
messageList
.
size
>
0
})
const
isSendBtnDisabled
=
computed
(()
=>
{
return
!
inputMessageContent
.
value
.
trim
()
})
const
inputPlaceholder
=
computed
(()
=>
{
return
isLogin
.
value
?
t
(
'common_module.dialogue_module.question_input_placeholder'
)
:
''
})
const
isCreateContinueQuestions
=
computed
(()
=>
{
return
props
.
continuousQuestionStatus
===
'default'
})
const
isInputMessageDisabled
=
computed
(()
=>
{
return
(
uploadFileList
.
value
.
some
((
fileItem
)
=>
fileItem
.
status
!==
UploadStatus
.
FINISHED
)
||
uploadImageList
.
value
.
some
((
imageItem
)
=>
imageItem
.
status
!==
UploadStatus
.
FINISHED
)
)
})
const
isUploadFileDisabled
=
computed
(()
=>
{
return
uploadFileList
.
value
.
length
===
1
})
watch
(
()
=>
uploadImageList
.
value
.
length
,
()
=>
{
emit
(
'updatePageScroll'
)
},
)
const
uploadFileIcon
=
(
type
:
string
)
=>
{
return
`https://gsst-poe-sit.gz.bcebos.com/icon/
${
type
}
.svg`
}
onMounted
(()
=>
{
emitter
?.
on
(
'selectQuestion'
,
(
featuredQuestion
)
=>
{
if
(
!
isLogin
.
value
)
{
window
.
$message
.
warning
(
t
(
'common_module.not_login_text'
))
return
}
inputMessageContent
.
value
=
featuredQuestion
handleMessageSend
()
})
})
onUnmounted
(()
=>
{
blockMessageResponse
()
emitter
?.
off
(
'selectQuestion'
)
})
function
messageItemFactory
():
ConversationMessageItem
{
return
{
timestamp
:
Date
.
now
(),
role
:
'user'
,
textContent
:
''
,
isEmptyContent
:
false
,
isTextContentLoading
:
false
,
isAnswerResponseLoading
:
false
,
isVoiceLoading
:
false
,
isVoicePlaying
:
false
,
voiceFragmentUrlList
:
[],
pluginName
:
''
,
imageUrl
:
''
,
reasoningContent
:
''
,
knowledgeContentResult
:
[],
}
}
function
handleInputMessageEnter
(
event
:
KeyboardEvent
)
{
if
(
event
.
key
===
'Enter'
&&
!
event
.
shiftKey
)
{
event
.
preventDefault
()
handleMessageSend
()
}
}
function
handleMessageSend
(
lastQuestionContent
?:
string
)
{
if
(
!
isLogin
.
value
)
{
window
.
$message
.
warning
(
t
(
'common_module.not_login_text'
))
return
}
if
(
lastQuestionContent
)
{
inputMessageContent
.
value
=
lastQuestionContent
}
if
(
!
inputMessageContent
.
value
.
trim
()
||
isInputMessageDisabled
.
value
)
{
return
}
if
(
isAnswerResponseWait
.
value
||
messageAudioLoading
.
value
)
{
window
.
$message
.
warning
(
t
(
'common_module.dialogue_module.do_not_operate_until_the_reply_is_complete'
))
return
}
if
(
props
.
answerAudioPlaying
)
{
window
.
$message
.
warning
(
t
(
'common_module.dialogue_module.stop_playing_and_then_operate'
))
return
}
const
latestUserMessageKey
=
nanoid
()
const
latestAssistantMessageKey
=
nanoid
()
currentLatestMessageItemKeyMap
.
value
.
set
(
'user'
,
latestUserMessageKey
)
currentLatestMessageItemKeyMap
.
value
.
set
(
'assistant'
,
latestAssistantMessageKey
)
emit
(
'resetContinueQuestionList'
)
emit
(
'addMessageItem'
,
latestUserMessageKey
,
{
...
messageItemFactory
(),
textContent
:
inputMessageContent
.
value
,
imageUrl
:
uploadImageList
.
value
?.[
0
]?.
url
||
''
,
})
emit
(
'updatePageScroll'
)
emit
(
'addMessageItem'
,
latestAssistantMessageKey
,
{
...
messageItemFactory
(),
role
:
'assistant'
,
isTextContentLoading
:
true
,
isAnswerResponseLoading
:
true
,
isVoiceLoading
:
true
,
})
emit
(
'updatePageScroll'
)
let
replyTextContent
=
''
let
reasoningContent
=
''
isAnswerResponseWait
.
value
=
true
isAnswerResponseInterrupt
.
value
=
false
currentReplyContentSentenceExtractIndex
.
value
=
0
sentenceFragmentSerialNo
.
value
=
0
sentenceExtractCheckEnabled
.
value
=
false
assistantFullAnswerContent
.
value
=
''
sentenceSpeechException
.
value
=
false
messageAudioLoading
.
value
=
false
controller
=
new
AbortController
()
fetchCustomEventSource
({
path
:
'/api/rest/agentApplicationRest/callAgentApplication.json'
,
payload
:
{
agentId
:
props
.
agentId
,
dialogsId
:
props
.
dialogsId
,
fileUrls
:
uploadFileList
.
value
.
map
((
item
)
=>
item
.
url
),
input
:
inputMessageContent
.
value
,
channel
:
query
.
channel
||
ChannelType
.
link_share
,
imageUrl
:
uploadImageList
.
value
?.[
0
]?.
url
||
''
,
},
controller
,
onResponse
:
(
data
)
=>
{
// 推理内容
if
(
data
.
reasoningContent
)
{
reasoningContent
+=
data
.
reasoningContent
emit
(
'updateSpecifyMessageItem'
,
latestAssistantMessageKey
,
{
reasoningContent
})
emit
(
'updatePageScroll'
)
return
}
// 插件
if
(
data
.
function
&&
data
.
function
.
name
)
{
emit
(
'updateSpecifyMessageItem'
,
latestAssistantMessageKey
,
{
pluginName
:
data
.
function
.
name
})
emit
(
'updatePageScroll'
)
return
}
// 回复内容
if
(
data
.
message
)
{
replyTextContent
+=
data
.
message
emit
(
'updateSpecifyMessageItem'
,
latestAssistantMessageKey
,
{
textContent
:
replyTextContent
,
isTextContentLoading
:
false
,
})
emit
(
'updatePageScroll'
)
assistantFullAnswerContent
.
value
=
(
assistantFullAnswerContent
.
value
+
data
.
message
).
replace
(
/
\^\[[\d\\
[
\]
-
]
+
?\]\^
/g
,
''
,
)
if
(
!
sentenceExtractCheckEnabled
.
value
&&
props
.
isEnableVoice
)
{
sentenceExtract
(
latestAssistantMessageKey
)
sentenceExtractCheckEnabled
.
value
=
true
messageAudioLoading
.
value
=
true
}
}
},
onRequestError
:
()
=>
{
errorMessageResponse
()
},
onError
:
()
=>
{
errorMessageResponse
()
},
onFinally
:
()
=>
{
emit
(
'updateSpecifyMessageItem'
,
latestAssistantMessageKey
,
{
isEmptyContent
:
!
replyTextContent
,
isTextContentLoading
:
false
,
isAnswerResponseLoading
:
false
,
})
isCreateContinueQuestions
.
value
&&
emit
(
'createContinueQuestions'
,
replyTextContent
)
emit
(
'updatePageScroll'
)
isAnswerResponseWait
.
value
=
false
controller
=
null
userStore
.
isLogin
&&
userStore
.
fetchUpdateEquityInfo
()
},
})
inputMessageContent
.
value
=
''
uploadImageList
.
value
=
[]
}
function
errorMessageResponse
()
{
emit
(
'updateSpecifyMessageItem'
,
currentLatestMessageItemKeyMap
.
value
.
get
(
'assistant'
)
!
,
{
isTextContentLoading
:
false
,
textContent
:
''
,
})
emit
(
'deleteMessageItem'
,
currentLatestMessageItemKeyMap
.
value
.
get
(
'user'
)
!
)
emit
(
'deleteMessageItem'
,
currentLatestMessageItemKeyMap
.
value
.
get
(
'assistant'
)
!
)
emit
(
'audioPause'
)
blockMessageResponse
()
}
function
handleClearAllMessage
()
{
if
(
!
isAllowClearMessage
.
value
)
return
emit
(
'clearAllMessage'
)
}
function
blockMessageResponse
()
{
controller
?.
abort
()
isAnswerResponseWait
.
value
=
false
messageAudioLoading
.
value
=
false
userStore
.
isLogin
&&
userStore
.
fetchUpdateEquityInfo
()
}
function
handleToLogin
()
{
emit
(
'toLogin'
)
}
function
handleSelectFile
(
cb
:
()
=>
void
)
{
if
(
isUploadFileDisabled
.
value
)
{
window
.
$message
.
ctWarning
(
''
,
t
(
'common_module.dialogue_module.overwrite_file_tip'
)).
then
(()
=>
{
cb
()
})
return
}
cb
()
}
function
handleSelectImage
(
cb
:
()
=>
void
)
{
if
(
uploadImageList
.
value
.
length
>
0
)
{
showDialog
({
title
:
''
,
message
:
t
(
'common_module.dialogue_module.overwrite_file_tip'
),
showCancelButton
:
true
,
cancelButtonText
:
t
(
'common_module.cancel_btn_text'
),
confirmButtonText
:
t
(
'common_module.confirm_btn_text'
),
confirmButtonColor
:
'#F25744'
,
}).
then
(()
=>
{
cb
()
})
return
}
cb
()
}
function
sentenceExtract
(
messageId
:
string
)
{
const
symbolRegExp
=
/
[
。!?;.!?;~
]
/g
let
sentenceDraft
=
assistantFullAnswerContent
.
value
.
replace
(
/
\s{5,}
/gi
,
''
)
.
slice
(
currentReplyContentSentenceExtractIndex
.
value
)
let
matchResult
=
symbolRegExp
.
exec
(
sentenceDraft
)
function
matchExtract
()
{
const
article
=
assistantFullAnswerContent
.
value
.
replace
(
/
\s{5,}
/gi
,
''
)
if
(
matchResult
&&
matchResult
.
index
&&
matchResult
.
index
>
60
)
{
sentenceDraft
=
article
.
slice
(
currentReplyContentSentenceExtractIndex
.
value
,
currentReplyContentSentenceExtractIndex
.
value
+
matchResult
.
index
+
matchResult
[
'0'
].
length
,
)
currentReplyContentSentenceExtractIndex
.
value
+=
sentenceDraft
.
length
ttsSocketSendText
(
sentenceDraft
,
sentenceFragmentSerialNo
.
value
,
messageId
)
sentenceFragmentSerialNo
.
value
+=
1
if
(
article
.
length
-
currentReplyContentSentenceExtractIndex
.
value
>
60
)
{
sentenceDraft
=
article
.
slice
(
currentReplyContentSentenceExtractIndex
.
value
)
matchResult
=
symbolRegExp
.
exec
(
sentenceDraft
)
matchExtract
()
}
else
{
setTimeout
(()
=>
sentenceExtract
(
messageId
),
600
)
}
}
else
if
(
!
isAnswerResponseWait
.
value
)
{
/* 延时避免最后回复内容没有更新全 */
setTimeout
(()
=>
{
sentenceDraft
=
article
.
slice
(
currentReplyContentSentenceExtractIndex
.
value
)
ttsSocketSendText
(
sentenceDraft
,
sentenceFragmentSerialNo
.
value
,
messageId
)
sentenceFragmentSerialNo
.
value
+=
1
},
700
)
}
else
{
sentenceDraft
=
assistantFullAnswerContent
.
value
.
replace
(
/
\s{5,}
/gi
,
''
)
.
slice
(
currentReplyContentSentenceExtractIndex
.
value
)
matchResult
=
symbolRegExp
.
exec
(
sentenceDraft
)
setTimeout
(()
=>
matchExtract
(),
500
)
}
}
if
(
matchResult
)
matchExtract
()
else
if
(
!
isAnswerResponseWait
.
value
)
matchExtract
()
else
setTimeout
(()
=>
sentenceExtract
(
messageId
),
600
)
}
function
ttsSocketSendText
(
text
:
string
,
audioUrlSerialNo
:
number
,
messageId
:
string
)
{
if
(
sentenceSpeechException
.
value
)
{
return
}
const
ttsSocketCtl
=
new
WebSocketCtr
(
TEXTTOSPEECH_WS_URL
)
ttsSocketCtl
.
onMessage
=
(
data
:
{
audio
:
string
;
replyVoiceUrl
:
string
;
final
:
boolean
})
=>
{
if
(
data
.
replyVoiceUrl
)
{
if
(
props
.
messageList
.
get
(
messageId
)?.
voiceFragmentUrlList
)
{
const
voiceFragmentUrlListDraft
=
[...
props
.
messageList
.
get
(
messageId
)
!
.
voiceFragmentUrlList
]
voiceFragmentUrlListDraft
[
audioUrlSerialNo
]
=
data
.
replyVoiceUrl
messageAudioLoading
.
value
=
false
emit
(
'updateSpecifyMessageItem'
,
messageId
,
{
voiceFragmentUrlList
:
voiceFragmentUrlListDraft
})
if
(
props
.
answerAudioAutoPlay
&&
audioUrlSerialNo
===
0
&&
voiceFragmentUrlListDraft
[
audioUrlSerialNo
])
{
emit
(
'audioPlay'
,
props
.
messageList
.
get
(
messageId
)
!
)
}
if
(
!
props
.
answerAudioAutoPlay
)
{
emit
(
'updateSpecifyMessageItem'
,
messageId
,
{
isVoiceLoading
:
false
})
}
}
}
}
ttsSocketCtl
.
onMessageError
=
()
=>
{
emit
(
'updateSpecifyMessageItem'
,
messageId
,
{
isVoiceLoading
:
false
,
voiceFragmentUrlList
:
[]
})
props
.
messageList
.
get
(
messageId
)?.
isVoicePlaying
&&
emit
(
'audioPause'
)
sentenceSpeechException
.
value
=
true
messageAudioLoading
.
value
=
false
window
.
$message
.
error
(
t
(
'common_module.unplayable_tip'
))
}
const
content
=
(
text
||
''
).
replace
(
/
\^\[[\d\\
[
\]
-
]
+
?\]\^
/g
,
''
)
if
(
content
&&
props
.
timbreId
)
{
ttsSocketCtl
.
connect
(()
=>
{
ttsSocketCtl
.
send
({
codec
:
'wav'
,
sampleRate
:
16000
,
speed
:
0
,
voiceType
:
props
.
timbreId
,
volume
:
0
,
content
,
})
})
}
}
defineExpose
({
blockMessageResponse
,
errorMessageResponse
,
handleMessageSend
,
})
</
script
>
<
template
>
<div
class=
"my-[7px]"
>
<div
class=
"flex items-end gap-2.5"
>
<n-upload
:show-file-list=
"false"
accept=
".doc, .pdf, .docx, .txt, .md"
:disabled=
"!isLogin"
abstract
@
before-upload=
"handleLimitUpload"
@
change=
"handleUpload"
>
<n-upload-trigger
#="
{ handleClick }" abstract>
<n-popover
style=
"width: 210px"
trigger=
"hover"
>
<template
#
trigger
>
<div
v-show=
"isEnableDocumentParse"
class=
"flex h-[35px] w-[35px] items-center justify-center rounded-full bg-[#F2F2F2]"
:class=
"
isLogin ? 'text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed text-[#b8babf]'
"
@
click=
"handleSelectFile(handleClick)"
>
<i
class=
"iconfont icon-upload flex h-4 w-4 items-center justify-center"
/>
</div>
</
template
>
<span
class=
"text-xs"
>
{{ t('common_module.dialogue_module.upload_file_limit') }}
</span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-upload
:show-file-list=
"false"
accept=
"image/png, image/jpeg, image/jpg, image/webp"
abstract
@
before-upload=
"handleLimitUploadImage"
@
change=
"handleUploadImage"
>
<n-upload-trigger
#="{
handleClick
}"
abstract
>
<n-popover
style=
"width: 210px"
trigger=
"hover"
>
<
template
#
trigger
>
<div
v-show=
"isEnableUploadImage"
class=
"flex h-[35px] w-[35px] items-center justify-center rounded-full bg-[#F2F2F2]"
:class=
"
isLogin ? 'text-theme-color cursor-pointer hover:opacity-80' : 'cursor-not-allowed text-[#b8babf]'
"
@
click=
"handleSelectImage(handleClick)"
>
<i
class=
"iconfont icon-upload-image flex h-4 w-4 items-center justify-center"
/>
</div>
</
template
>
<span
class=
"text-xs"
>
{{ t('common_module.dialogue_module.upload_image_limit') }}
</span>
</n-popover>
</n-upload-trigger>
</n-upload>
<n-popover
trigger=
"hover"
>
<
template
#
trigger
>
<div
class=
"flex h-[35px] w-[35px] items-center justify-center rounded-full bg-[#F2F2F2]"
:class=
"
isAllowClearMessage
? 'text-theme-color cursor-pointer hover:opacity-80'
: 'cursor-not-allowed text-[#b8babf]'
"
@
click=
"handleClearAllMessage"
>
<i
class=
"iconfont icon-clear text-base leading-none"
/>
</div>
</
template
>
<span
class=
"text-xs"
>
{{ t('common_module.dialogue_module.clear_message_popover_message') }}
</span>
</n-popover>
<div
class=
"flex flex-1 flex-col"
>
<div
class=
"flex gap-[14px]"
>
<div
v-for=
"uploadImageItem in uploadImageList"
:key=
"uploadImageItem.id"
class=
"border-inactive-border-color relative mb-1.5 h-[48px] w-[48px] rounded-[10px] border bg-white"
:class=
"{ 'border-[#F25744]!': uploadImageItem.status === UploadStatus.ERROR }"
>
<div
class=
"absolute right-[-4px] top-[-4px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.55)] hover:opacity-80"
@
click=
"handleRemoveUploadImage(uploadImageItem.id)"
>
<CloseSmall
theme=
"outline"
size=
"16"
fill=
"#fff"
/>
</div>
<div
class=
"flex h-full w-full items-center justify-center overflow-hidden rounded-[10px]"
>
<n-spin
v-show=
"uploadImageItem.status === UploadStatus.UPLOADING"
:size=
"20"
/>
<n-image
v-show=
"uploadImageItem.status === UploadStatus.FINISHED"
object-fit=
"contain"
:src=
"uploadImageItem.url"
preview-disabled
class=
"h-full w-full flex-shrink-0"
/>
</div>
</div>
</div>
<ul
v-show=
"uploadFileList.length > 0"
class=
"mb-1.5 grid gap-1.5"
>
<li
v-for=
"uploadFileItem in uploadFileList"
:key=
"uploadFileItem.id"
class=
"group relative flex h-[42px] w-full items-center overflow-hidden rounded-[10px] border bg-white/70"
:class=
"uploadFileItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'"
>
<div
class=
"flex w-full items-center justify-between"
>
<div
class=
"flex w-full items-center overflow-hidden"
>
<img
:src=
"uploadFileIcon(uploadFileItem.type!)"
class=
"h-6 w-6"
/>
<div
class=
"mx-2 flex flex-1 flex-col overflow-hidden text-[12px]"
>
<n-ellipsis>
{{ uploadFileItem.name }}
</n-ellipsis>
</div>
</div>
<n-progress
v-show=
"!['finished', 'error'].includes(uploadFileItem.status)"
class=
"left-13.5 w-[calc(100%-78px)]! absolute bottom-0"
type=
"line"
rail-color=
"#F3F3F3"
:height=
"4"
:percentage=
"uploadFileItem.percentage"
:show-indicator=
"false"
/>
<div
v-show=
"['finished', 'error'].includes(uploadFileItem.status)"
class=
"group-hover:block"
:class=
"isMobile ? 'block' : 'hidden'"
>
<n-popover
trigger=
"hover"
placement=
"top-end"
:show-arrow=
"false"
>
<
template
#
trigger
>
<i
class=
"iconfont icon-close cursor-pointer text-[14px] hover:opacity-80"
:class=
"uploadFileItem.status === 'error' ? 'text-error-font-color' : 'text-font-color'"
@
click=
"handleRemoveFile(uploadFileItem.id)"
/>
</
template
>
<span>
{{ t('common_module.dialogue_module.cancel_associate_file_tip') }}
</span>
</n-popover>
</div>
</div>
</li>
</ul>
<div
class=
"relative flex-1"
>
<n-input
v-model:value=
"inputMessageContent"
type=
"textarea"
:autosize=
"{ minRows: 1, maxRows: 5 }"
:placeholder=
"inputPlaceholder"
:disabled=
"!isLogin || isInputMessageDisabled"
class=
"question-textarea rounded-xl! shadow-[0_1px_#09122105,0_1px_1px_#09122105,0_3px_3px_#09122103,0_9px_9px_#09122103]! bg-[#F2F2F2]! min-h-[35px] pr-[50px]"
@
keydown=
"handleInputMessageEnter"
/>
<div
class=
"bg-px-send-png absolute bottom-[8px] right-[16px] h-[18px] w-[18px]"
:class=
"
isSendBtnDisabled ||
isAnswerResponseWait ||
!isLogin ||
isInputMessageDisabled ||
answerAudioPlaying ||
messageAudioLoading
? 'opacity-60'
: 'cursor-pointer'
"
@
click=
"handleMessageSend()"
/>
<div
v-show=
"!isLogin"
class=
"absolute left-3 top-[8px] flex h-[19px] max-w-[calc(100%-60px)] items-center overflow-hidden text-[12px] leading-[19px] text-[#84868c]"
>
<span
class=
"shrink-0"
>
{{ t('share_agent_module.please') }}
</span>
<span
class=
"text-theme-color shrink-0 cursor-pointer px-1 hover:opacity-80"
@
click=
"handleToLogin"
>
{{ t('common_module.login') }}
</span>
<span
class=
"shrink-0"
>
{{ t('share_agent_module.after_action') }}
</span>
</div>
</div>
</div>
</div>
<div
class=
"mt-[9px]"
>
<span
class=
"flex w-full justify-center text-xs text-[#b8babf]"
>
{{ t('common_module.dialogue_module.generate_warning_message') }}
</span>
</div>
</div>
</template>
<
style
lang=
"scss"
scoped
>
:deep
(
.question-textarea.n-input--textarea
)
{
--n-border
:
none
!
important
;
--n-border-focus
:
none
!
important
;
--n-border-hover
:
none
!
important
;
--n-box-shadow-focus
:
none
!
important
;
--n-border-disabled
:
none
!
important
;
.n-input__textarea-el
,
.n-input__placeholder
{
line-height
:
24px
;
}
}
</
style
>
src/views/share/components/mobile/message-item.vue
0 → 100644
View file @
6d7b65ec
<
script
setup
lang=
"ts"
>
import
{
computed
,
ref
,
useTemplateRef
}
from
'vue'
import
{
useI18n
}
from
'vue-i18n'
import
{
CheckOne
,
Down
}
from
'@icon-park/vue-next'
import
CustomLoading
from
'../custom-loading.vue'
import
MusicWavesLoading
from
'../music-waves-loading.vue'
import
MarkdownRender
from
'@/components/markdown-render/markdown-render.vue'
import
{
PersonalAppConfigState
}
from
'@/store/types/personal-app-config'
interface
Props
{
role
:
'user'
|
'assistant'
messageItem
:
ConversationMessageItem
agentApplicationConfig
:
PersonalAppConfigState
}
const
{
t
}
=
useI18n
()
const
props
=
defineProps
<
Props
>
()
const
emit
=
defineEmits
<
{
audioPlay
:
[]
audioPause
:
[]
}
>
()
const
markdownRenderRef
=
useTemplateRef
<
InstanceType
<
typeof
MarkdownRender
>>
(
'markdownRenderRef'
)
const
isShowReasoningContent
=
ref
(
true
)
const
timbreEnabled
=
computed
(()
=>
{
return
!!
props
.
agentApplicationConfig
.
voiceConfig
.
timbreId
})
const
isShowAudioControl
=
computed
(()
=>
{
return
props
.
role
===
'assistant'
&&
!
props
.
messageItem
.
isVoiceLoading
&&
timbreEnabled
.
value
})
const
isPlayableAudio
=
computed
(()
=>
{
return
isShowAudioControl
.
value
&&
!!
props
.
messageItem
.
voiceFragmentUrlList
.
length
})
const
isShowVoiceLoading
=
computed
(()
=>
{
return
(
props
.
role
===
'assistant'
&&
(
props
.
messageItem
.
isAnswerResponseLoading
||
(
props
.
messageItem
.
isVoiceLoading
&&
timbreEnabled
.
value
))
)
})
const
isDeepSeekR1
=
computed
(()
=>
{
return
props
.
agentApplicationConfig
.
commModelConfig
.
largeModel
===
'DeepSeek'
})
function
handleAudioControl
()
{
if
(
!
isPlayableAudio
.
value
)
{
return
}
if
(
props
.
messageItem
.
isVoicePlaying
)
{
emit
(
'audioPause'
)
}
else
{
emit
(
'audioPlay'
)
}
}
function
handleShowReasoningContentSwitch
()
{
isShowReasoningContent
.
value
=
!
isShowReasoningContent
.
value
}
</
script
>
<
template
>
<div
class=
"mb-[15px] flex flex-row-reverse last:mb-0"
:class=
"[role === 'assistant' ? 'justify-end' : 'justify-start']"
>
<div
class=
"flex w-full flex-col overflow-x-auto"
:class=
"[role === 'user' ? 'items-end' : 'items-start']"
>
<!-- 大模型深度思考 -->
<template
v-if=
"role === 'assistant' && isDeepSeekR1"
>
<div
class=
"my-[7px] select-none text-[12px]"
>
<div
class=
"inline-flex cursor-pointer"
@
click=
"handleShowReasoningContentSwitch"
>
<span
v-if=
"messageItem.isTextContentLoading"
class=
"mr-[6px]"
>
{{
t
(
'common_module.deep_thinking'
,
{
modelName
:
agentApplicationConfig
.
commModelConfig
.
largeModel
}
)
}}
<
/span
>
<
span
v
-
else
class
=
"mr-[6px]"
>
{{
t
(
'common_module.have_thought_deeply'
)
}}
<
/span
>
<
Down
theme
=
"outline"
:
size
=
"18"
fill
=
"#333"
:
stroke
-
width
=
"3"
class
=
"transition-[rotate] duration-100 ease-linear"
:
class
=
"{ '-rotate-180': isShowReasoningContent
}
"
/>
<
/div
>
<
/div
>
<
n
-
collapse
-
transition
:
show
=
"isShowReasoningContent"
>
<
div
v
-
if
=
"messageItem.reasoningContent"
class
=
"my-[10px] border-l-[1px] border-solid border-l-[#ccc] px-[13px] py-[8px]"
>
<
MarkdownRender
ref
=
"markdownRenderRef"
:
raw
-
text
-
content
=
"
messageItem.reasoningContent
? messageItem.reasoningContent
: t('common_module.dialogue_module.empty_message_content')
"
color
=
"#999"
:
font
-
size
=
"'12px'"
/>
<
/div
>
<
/n-collapse-transition
>
<
/template
>
<!--
模型内容
-->
<
div
class
=
"flex min-w-[80px] max-w-full flex-col"
>
<
div
class
=
"w-full flex-wrap rounded-xl border px-4 px-[12px] py-[11px]"
:
class
=
"[
{ 'rounded-tr-none border-[#DCDEFF] bg-[#DCDEFF] text-white': role === 'user'
}
,
{ 'rounded-tl-none border-[#e8e9eb] bg-white text-[#333]': role === 'assistant'
}
,
]"
>
<
img
v
-
show
=
"role === 'user' && messageItem.imageUrl"
:
src
=
"messageItem.imageUrl"
class
=
"max-h-[120px]! mb-[11px] rounded-[10px] object-contain"
/>
<
div
v
-
show
=
"role === 'assistant' && messageItem.pluginName"
class
=
"mb-[8px] flex items-center gap-[5px] font-['Microsoft_YaHei_UI'] text-[#999]"
>
<
div
v
-
show
=
"messageItem.isTextContentLoading"
class
=
"bg-px-plugin_loading-gif h-[14px] w-[14px] bg-contain bg-center bg-no-repeat"
/>
<
CheckOne
v
-
show
=
"!messageItem.isTextContentLoading"
theme
=
"outline"
:
class
=
"'14'"
fill
=
"#40bd23"
/>
<
span
class
=
"text-[12px] leading-5"
>
{{
messageItem
.
isTextContentLoading
?
t
(
'common_module.plugin_in_progress'
,
{
pluginName
:
messageItem
.
pluginName
}
)
:
t
(
'common_module.plugin_executed_successfully'
,
{
pluginName
:
messageItem
.
pluginName
}
)
}}
<
/span
>
<
/div
>
<
div
v
-
if
=
"messageItem.isTextContentLoading"
class
=
"py-1.5 pl-4"
>
<
CustomLoading
:
active
-
color
=
"'#000DFF'"
/>
<
/div
>
<
div
v
-
else
>
<
p
class
=
"break-all"
>
<
MarkdownRender
ref
=
"markdownRenderRef"
:
raw
-
text
-
content
=
"
messageItem.isEmptyContent
? t('common_module.dialogue_module.empty_message_content')
: messageItem.textContent
"
:
color
=
"'#333'"
:
font
-
size
=
"'12px'"
/>
<
/p
>
<
div
v
-
show
=
"isShowVoiceLoading || messageItem.isAnswerResponseLoading"
class
=
"mb-[5px] mt-4 px-4"
>
<
CustomLoading
:
active
-
color
=
"'#000DFF'"
/>
<
/div
>
<
/div
>
<!--
移动端语音播放
-->
<
div
v
-
show
=
"isShowAudioControl"
class
=
"mt-[13px] flex items-center gap-2"
>
<
div
class
=
"h-[18px] w-[18px] cursor-pointer"
:
class
=
"messageItem.isVoicePlaying ? 'bg-svg-pause' : 'bg-svg-play'"
@
click
=
"handleAudioControl"
/>
<
MusicWavesLoading
v
-
show
=
"messageItem.isVoicePlaying && isPlayableAudio"
bar
-
bg
-
color
=
"#333"
/>
<
n
-
popover
style
=
"max-width: 310px"
>
<
template
#
trigger
>
<
span
v
-
show
=
"!isPlayableAudio"
class
=
"text-[12px]"
>
{{
t
(
'common_module.unplayable'
)
}}
<
/span
>
<
/template
>
{{
t
(
'common_module.unplayable_tip'
)
}}
<
/n-popover
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/template
>
src/views/share/components/mobile/message-list.vue
0 → 100644
View file @
6d7b65ec
<
script
setup
lang=
"ts"
>
import
{
computed
,
nextTick
,
watch
}
from
'vue'
import
{
useI18n
}
from
'vue-i18n'
import
{
useScroll
}
from
'@/composables/useScroll'
import
{
PersonalAppConfigState
}
from
'@/store/types/personal-app-config'
import
{
useBackBottom
}
from
'@/composables/useBackBottom'
import
MessageItem
from
'./message-item.vue'
import
ContinueQuestion
from
'./continue-question.vue'
interface
Props
{
messageList
:
Map
<
string
,
ConversationMessageItem
>
agentApplicationConfig
:
PersonalAppConfigState
continuousQuestionStatus
:
'default'
|
'close'
isAnswerResponseLoading
:
boolean
createContinueQuestionsException
:
boolean
isAnswerResponseInterrupt
?:
boolean
}
const
props
=
defineProps
<
Props
>
()
defineEmits
<
{
audioPlay
:
[
messageItem
:
ConversationMessageItem
]
audioPause
:
[]
}
>
()
const
{
t
}
=
useI18n
()
const
{
scrollRef
,
scrollToBottom
}
=
useScroll
()
const
{
visible
,
clickBackBottom
,
throttleScrollContainer
}
=
useBackBottom
(
scrollRef
,
scrollToBottom
)
const
continuousQuestionList
=
defineModel
<
string
[]
>
(
'continuousQuestionList'
,
{
required
:
true
})
const
isShowContinueQuestion
=
computed
(()
=>
{
return
(
props
.
continuousQuestionStatus
===
'default'
&&
props
.
messageList
.
size
>
1
&&
!
props
.
isAnswerResponseLoading
&&
!
props
.
createContinueQuestionsException
&&
!
props
.
isAnswerResponseInterrupt
)
})
watch
(
()
=>
props
.
messageList
.
size
,
()
=>
{
clickBackBottom
()
},
)
defineExpose
({
scrollToBottom
:
handleScrollToBottom
,
})
function
handleScrollToBottom
()
{
nextTick
(()
=>
{
!
visible
.
value
&&
scrollToBottom
()
})
}
</
script
>
<
template
>
<main
ref=
"scrollRef"
class=
"h-full overflow-y-auto overflow-x-hidden px-5"
@
scroll=
"throttleScrollContainer"
>
<div>
<MessageItem
v-for=
"[key, messageItem] in messageList"
:key=
"key"
:role=
"messageItem.role"
:message-item=
"messageItem"
:agent-application-config=
"agentApplicationConfig"
@
audio-play=
"() => $emit('audioPlay', messageItem)"
@
audio-pause=
"() => $emit('audioPause')"
/>
</div>
<p
v-show=
"isAnswerResponseLoading"
class=
"my-[7px] ml-1 text-xs text-[#84868c]"
>
{{
t
(
'common_module.dialogue_module.do_not_exit_page'
)
}}
</p>
<div
v-show=
"isShowContinueQuestion"
>
<ContinueQuestion
v-model:continuous-question-list=
"continuousQuestionList"
:type=
"'continuous'"
/>
</div>
<div
v-show=
"visible"
class=
"flex-center hover:text-theme-color absolute bottom-5 right-5 h-6 w-6 cursor-pointer rounded-full bg-white shadow-[0_0_0_1px_#ededed]"
@
click
.
stop=
"clickBackBottom"
>
<i
class=
"iconfont icon-left rotate-270 text-sm"
/>
</div>
</main>
</
template
>
src/views/share/components/mobile/page-header.vue
0 → 100644
View file @
6d7b65ec
<
script
setup
lang=
"ts"
>
import
{
useUserStore
}
from
'@/store/modules/user'
import
{
computed
,
CSSProperties
}
from
'vue'
import
{
useI18n
}
from
'vue-i18n'
import
{
Logout
}
from
'@icon-park/vue-next'
interface
Props
{
agentTitle
:
string
}
const
{
t
}
=
useI18n
()
defineProps
<
Props
>
()
const
emit
=
defineEmits
<
{
toCreateApplication
:
[]
toLogin
:
[]
toLogout
:
[]
updateAutoPlaying
:
[
value
:
boolean
]
}
>
()
const
userStore
=
useUserStore
()
const
isEnableVoice
=
defineModel
<
boolean
>
(
'isEnableVoice'
,
{
required
:
true
})
const
answerAudioAutoPlay
=
defineModel
<
boolean
>
(
'answerAudioAutoPlay'
,
{
required
:
true
})
const
railStyle
=
({
focused
,
checked
}:
{
focused
:
boolean
;
checked
:
boolean
})
=>
{
const
style
:
CSSProperties
=
{}
if
(
checked
)
{
style
.
background
=
'#CFD1FF'
}
else
{
if
(
focused
)
{
style
.
boxShadow
=
'0 0 0 2px #ddd'
}
}
return
style
}
const
isLogin
=
computed
(()
=>
{
return
userStore
.
isLogin
})
function
handleToCreateApplication
()
{
emit
(
'toCreateApplication'
)
}
function
handleToLogin
()
{
emit
(
'toLogin'
)
}
function
handleToLogout
()
{
emit
(
'toLogout'
)
}
</
script
>
<
template
>
<header
class=
"flex h-[58px] w-full items-center justify-between px-[20px]"
>
<div>
<div
v-show=
"isEnableVoice"
class=
"flex items-center gap-2"
>
<span
class=
"text-[12px]"
>
{{
t
(
'common_module.voice_auto_play'
)
}}
</span>
<n-switch
v-model:value=
"answerAudioAutoPlay"
size=
"small"
:rail-style=
"railStyle"
@
update:value=
"(isAutoPlay: boolean) => emit('updateAutoPlaying', isAutoPlay)"
>
<template
#
checked
>
{{
t
(
'common_module.open'
)
}}
</
template
>
<
template
#
unchecked
>
{{
t
(
'common_module.close'
)
}}
</
template
>
<
template
#
checked-icon
>
<div
class=
"bg-theme-color h-full w-full rounded-full"
></div>
</
template
>
</n-switch>
</div>
</div>
<div
class=
"flex-center gap-[10px]"
>
<NButton
v-show=
"isLogin"
type=
"primary"
color=
"#EBECFF"
class=
"rounded-md! h-[28px]! text-[12px]! min-w-[80px]! text-theme-color!"
@
click=
"handleToCreateApplication"
>
{{ t('common_module.create_agent_btn_text') }}
</NButton>
<NButton
v-show=
"!isLogin"
color=
"#EBECFF"
class=
"rounded-md! h-[28]! text-[12px]! min-w-[80px]! text-theme-color!"
@
click=
"handleToLogin"
>
<span
class=
"text-xs"
>
{{ t('common_module.login_now') }}
</span>
</NButton>
<div
v-show=
"isLogin"
class=
"flex-center h-[28px] w-[28px] rounded-[5px] bg-[#EBECFF]"
>
<Logout
theme=
"outline"
size=
"15"
fill=
"#000DFF"
:stroke-width=
"4"
@
click=
"handleToLogout"
/>
</div>
</div>
</header>
</template>
src/views/share/components/mobile/preamble.vue
0 → 100644
View file @
6d7b65ec
<
script
setup
lang=
"ts"
>
import
{
computed
}
from
'vue'
import
{
PersonalAppConfigState
}
from
'@/store/types/personal-app-config'
import
ContinueQuestion
from
'./continue-question.vue'
interface
Props
{
agentApplicationConfig
:
PersonalAppConfigState
}
const
props
=
defineProps
<
Props
>
()
const
agentApplicationConfig
=
computed
(()
=>
props
.
agentApplicationConfig
)
const
agentAvatar
=
computed
(()
=>
{
return
(
agentApplicationConfig
.
value
.
baseInfo
.
agentAvatar
||
'https://gsst-poe-sit.gz.bcebos.com/data/20240911/1726041369632.webp'
)
})
</
script
>
<
template
>
<div
class=
"flex w-full flex-1 flex-col px-[20px]"
>
<div
class=
"mb-[7.5px] mt-[25px] flex w-full justify-center"
>
<img
:src=
"agentAvatar"
class=
"rounded-theme h-[40px] w-[40px] object-cover"
/>
</div>
<div
class=
"flex flex-col items-center justify-center"
>
<p
class=
"font-family-medium mb-[17px] line-clamp-1 text-[13px] text-[#333]"
>
{{
agentApplicationConfig
.
baseInfo
.
agentTitle
}}
</p>
<div
class=
"flex w-full flex-col items-start justify-center"
>
<p
v-show=
"agentApplicationConfig.commConfig.preamble"
class=
"mb-0 select-none break-all rounded-[10px] rounded-tl-none bg-[#DCDEFF] px-[12.5px] py-[11px] text-[12px]"
>
{{
agentApplicationConfig
.
commConfig
.
preamble
}}
</p>
<ContinueQuestion
v-model:continuous-question-list=
"agentApplicationConfig.commConfig.featuredQuestions"
:type=
"'featured'"
/>
</div>
</div>
</div>
</
template
>
src/views/share/share-application-mobile.vue
View file @
6d7b65ec
...
...
@@ -3,12 +3,14 @@ import { computed, onMounted, onUnmounted, ref, shallowRef } from 'vue'
import
{
useRouter
}
from
'vue-router'
import
{
useI18n
}
from
'vue-i18n'
import
{
Howl
}
from
'howler'
import
{
showDialog
}
from
'vant'
import
'vant/es/dialog/style'
import
{
useEventListener
}
from
'@vueuse/core'
import
type
{
ValueOf
}
from
'type-fest'
import
PageHeader
from
'./components/mobile
-
page-header.vue'
import
Preamble
from
'./components/preamble.vue'
import
MessageList
from
'./components/message-list.vue'
import
FooterInput
from
'./components/footer-input.vue'
import
PageHeader
from
'./components/mobile
/
page-header.vue'
import
Preamble
from
'./components/
mobile/
preamble.vue'
import
MessageList
from
'./components/m
obile/m
essage-list.vue'
import
FooterInput
from
'./components/
mobile/
footer-input.vue'
import
{
PersonalAppConfigState
}
from
'@/store/types/personal-app-config'
import
{
useUserStore
}
from
'@/store/modules/user'
import
{
defaultPersonalAppConfigState
}
from
'@/store/modules/personal-app-config'
...
...
@@ -160,10 +162,10 @@ function handleToLoginPage() {
}
function
handleCreateApplicationPage
()
{
window
.
$dialog
.
info
({
showDialog
({
title
:
t
(
'share_agent_module.create_agent_dialogue_title'
),
content
:
t
(
'share_agent_module.create_agent_dialogue_content'
),
positive
Text
:
t
(
'share_agent_module.create_agent_dialogue_positive_text'
),
message
:
t
(
'share_agent_module.create_agent_dialogue_content'
),
confirmButton
Text
:
t
(
'share_agent_module.create_agent_dialogue_positive_text'
),
})
}
...
...
@@ -204,18 +206,20 @@ function handleUpdatePageScroll() {
}
function
handleClearAllMessage
()
{
window
.
$message
.
ctWarning
(
t
(
'common_module.dialogue_module.clear_message_dialog_content'
),
t
(
'common_module.dialogue_module.clear_message_dialog_title'
),
)
.
then
(()
=>
{
handleAudioPause
()
footerInputRef
.
value
?.
blockMessageResponse
()
messageList
.
value
.
clear
()
answerAudioPlaying
.
value
=
false
window
.
$message
.
success
(
t
(
'common_module.clear_success_message'
))
})
showDialog
({
title
:
t
(
'common_module.dialogue_module.clear_message_dialog_title'
),
message
:
t
(
'common_module.dialogue_module.clear_message_dialog_content'
),
showCancelButton
:
true
,
cancelButtonText
:
t
(
'common_module.cancel_btn_text'
),
confirmButtonText
:
t
(
'common_module.confirm_btn_text'
),
confirmButtonColor
:
'#F25744'
,
}).
then
(()
=>
{
handleAudioPause
()
footerInputRef
.
value
?.
blockMessageResponse
()
messageList
.
value
.
clear
()
answerAudioPlaying
.
value
=
false
window
.
$message
.
success
(
t
(
'common_module.clear_success_message'
))
})
}
async
function
handleCreateContinueQuestions
(
replyTextContent
:
string
)
{
...
...
@@ -355,32 +359,43 @@ function handleExitPage() {
footerInputRef
.
value
?.
errorMessageResponse
()
}
}
function
handleToLogoutPage
()
{
showDialog
({
title
:
t
(
'login_module.confirm_to_logout'
),
message
:
t
(
'login_module.you_can_login_again_after_logout'
),
showCancelButton
:
true
,
cancelButtonText
:
t
(
'common_module.cancel_btn_text'
),
confirmButtonText
:
t
(
'common_module.logout'
),
confirmButtonColor
:
'#F25744'
,
}).
then
(()
=>
{
userStore
.
logout
()
handleAudioPause
()
footerInputRef
.
value
?.
blockMessageResponse
()
messageList
.
value
.
clear
()
answerAudioPlaying
.
value
=
false
})
}
</
script
>
<
template
>
<div
v-loading=
"fullScreenLoading"
class=
"
h-full w-full
"
>
<div
v-loading=
"fullScreenLoading"
class=
"
bg-px-share-h5_bg-png h-full w-full bg-cover bg-no-repeat
"
>
<PageHeader
v-model:is-enable-voice=
"isEnableVoice"
v-model:answer-audio-auto-play=
"answerAudioAutoPlay"
:agent-title=
"agentApplicationConfig.baseInfo.agentTitle"
@
to-login=
"handleToLoginPage"
@
to-logout=
"handleToLogoutPage"
@
to-create-application=
"handleCreateApplicationPage"
@
update-auto-playing=
"handleUpdateAutoPlaying"
/>
<div
class=
"flex h-[calc(100%-48px)] w-full flex-col bg-[#f2f5f9]"
>
<div
class=
"mt-5 flex select-none justify-end px-4"
>
<div
v-show=
"isEnableVoice"
class=
"flex items-center gap-2"
>
<span>
{{
t
(
'common_module.voice_auto_play'
)
}}
</span>
<n-switch
v-model:value=
"answerAudioAutoPlay"
size=
"small"
@
update:value=
"handleUpdateAutoPlaying"
>
<template
#
checked
>
{{
t
(
'common_module.open'
)
}}
</
template
>
<
template
#
unchecked
>
{{
t
(
'common_module.close'
)
}}
</
template
>
</n-switch>
</div>
</div>
<div
v-if=
"messageList.size === 0"
class=
"w-full flex-1 overflow-auto px-4"
>
<div
class=
"flex h-[calc(100%-58px)] w-full flex-col"
>
<div
v-if=
"messageList.size === 0"
class=
"w-full flex-1 overflow-auto"
>
<Preamble
:agent-application-config=
"agentApplicationConfig"
/>
</div>
<div
v-if=
"messageList.size > 0"
class=
"flex w-full flex-1 flex-col overflow-hidden
pt-5
"
>
<div
v-if=
"messageList.size > 0"
class=
"flex w-full flex-1 flex-col overflow-hidden"
>
<div
class=
"relative flex-1 overflow-auto"
>
<MessageList
ref=
"messageListRef"
...
...
@@ -391,13 +406,14 @@ function handleExitPage() {
:is-answer-response-loading=
"isAnswerResponseLoading"
:create-continue-questions-exception=
"createContinueQuestionsException"
:is-answer-response-interrupt=
"isAnswerResponseInterrupt"
class=
"pt-5"
@
audio-play=
"handleAudioPlay"
@
audio-pause=
"handleAudioPause"
/>
</div>
</div>
<div
class=
"footer-operation px-4"
>
<div
class=
"footer-operation
bg-white
px-4"
>
<FooterInput
ref=
"footerInputRef"
v-model:is-answer-response-loading=
"isAnswerResponseLoading"
...
...
types/locales.d.ts
View file @
6d7b65ec
...
...
@@ -44,6 +44,7 @@ declare namespace I18n {
publish_success_message
:
string
clear_success_message
:
string
add_success_message
:
string
reset_success_message
:
string
loading
:
string
updating
:
string
successful_update
:
string
...
...
@@ -151,6 +152,7 @@ declare namespace I18n {
not_certified_yet
:
string
authenticated
:
string
cancel_authorization
:
string
get_code
:
string
dialogue_module
:
{
continue_question_message
:
string
...
...
@@ -217,6 +219,7 @@ declare namespace I18n {
order_manage
:
string
data_statistic
:
string
plugin_center
:
string
reset_password
:
string
}
login_module
:
{
...
...
@@ -233,6 +236,26 @@ declare namespace I18n {
login_success
:
string
get_verification_code
:
string
other_login_methods
:
string
mobile_welcome_words
:
string
sign_in_with_mobile_number
:
string
verification_code_login
:
string
password_login
:
string
email_login
:
string
forgot_password
:
string
agreement_prefix
:
string
agreement_terms
:
string
agreement_privacy
:
string
agreement_and
:
string
confirm_to_logout
:
string
you_can_login_again_after_logout
:
string
}
reset_password_module
:
{
reset_login_password
:
string
please_enter_your_registered_phone_number
:
string
please_enter_your_new_password
:
string
back_to_login
:
string
reset_password
:
string
}
home_module
:
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment