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
6a91a552
Commit
6a91a552
authored
Apr 10, 2025
by
nick zheng
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 移动端登录&&重置密码&&应用分享页UI调整
parent
774da38e
Show 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 @
6a91a552
...
...
@@ -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 @
6a91a552
...
...
@@ -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 @
6a91a552
...
...
@@ -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 @
6a91a552
...
...
@@ -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 @
6a91a552
66.8 KB
src/assets/images/share/h5-bg.png
0 → 100644
View file @
6a91a552
13.1 KB
src/components/custom-modal/custom-modal.vue
View file @
6a91a552
...
...
@@ -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 @
6a91a552
...
...
@@ -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 @
6a91a552
...
...
@@ -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 @
6a91a552
...
...
@@ -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 @
6a91a552
...
...
@@ -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 @
6a91a552
...
...
@@ -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 @
6a91a552
<
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 @
6a91a552
<
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 @
6a91a552
<
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 @
6a91a552
<
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 @
774da38e
<
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 @
6a91a552
<
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 @
6a91a552
<
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 @
6a91a552
<
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 @
6a91a552
<
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 @
6a91a552
<
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 @
6a91a552
<
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 @
6a91a552
...
...
@@ -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,12 +206,14 @@ 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
(()
=>
{
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
()
...
...
@@ -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 @
6a91a552
...
...
@@ -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