首页调整
This commit is contained in:
parent
b65017c7a2
commit
e19615a89d
|
|
@ -89,4 +89,13 @@
|
|||
|
||||
<style>
|
||||
/*每个页面公共css */
|
||||
|
||||
/* 隐藏底部tabBar */
|
||||
uni-tabbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.uni-tabbar {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
439
xinlidsj/components/LoginModal.vue
Normal file
439
xinlidsj/components/LoginModal.vue
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
<template>
|
||||
<view v-if="visible" class="login-modal-overlay">
|
||||
<view class="login-modal-mask" @tap="onMaskClick"></view>
|
||||
<view class="login-modal-wrapper">
|
||||
<view class="login-modal-content">
|
||||
<view class="login-modal-header">
|
||||
<text class="login-modal-title">请登录</text>
|
||||
<text class="login-modal-close" @tap="onClose">×</text>
|
||||
</view>
|
||||
|
||||
<view class="login-modal-body">
|
||||
<view class="login-form-item">
|
||||
<text class="login-form-label">账号</text>
|
||||
<input
|
||||
class="login-form-input"
|
||||
type="text"
|
||||
v-model="username"
|
||||
placeholder="请输入账号"
|
||||
:focus="autoFocus"
|
||||
:adjust-position="true"
|
||||
confirm-type="next"
|
||||
@focus="onUsernameFocus"
|
||||
@blur="onUsernameBlur"
|
||||
@confirm="focusPassword"
|
||||
@tap.stop
|
||||
@click.stop
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="login-form-item">
|
||||
<text class="login-form-label">密码</text>
|
||||
<input
|
||||
class="login-form-input"
|
||||
type="text"
|
||||
v-model="password"
|
||||
:password="true"
|
||||
placeholder="请输入密码"
|
||||
:adjust-position="true"
|
||||
:focus="passwordFocus"
|
||||
confirm-type="done"
|
||||
@focus="onPasswordFocus"
|
||||
@blur="onPasswordBlur"
|
||||
@confirm="onLogin"
|
||||
@tap.stop
|
||||
@click.stop
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<view v-if="errorMsg" class="login-error-msg">
|
||||
<text class="error-icon">⚠️</text>
|
||||
<text class="error-text">{{ errorMsg }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 成功提示 -->
|
||||
<view v-if="successMsg" class="login-success-msg">
|
||||
<text class="success-icon">✓</text>
|
||||
<text class="success-text">{{ successMsg }}</text>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="login-modal-btn"
|
||||
type="primary"
|
||||
:disabled="loading"
|
||||
@tap.stop="onLogin"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
|
||||
<!-- 底部提示 -->
|
||||
<view class="login-modal-footer">
|
||||
<text class="footer-text">首次登录请联系管理员获取账号</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { setToken } from '../utils/auth'
|
||||
import { login } from '../api/system/login'
|
||||
|
||||
export default {
|
||||
name: 'LoginModal',
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
username: '',
|
||||
password: '',
|
||||
loading: false,
|
||||
autoFocus: false,
|
||||
passwordFocus: false,
|
||||
errorMsg: '',
|
||||
successMsg: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show: {
|
||||
handler(val) {
|
||||
console.log('LoginModal show changed:', val)
|
||||
this.visible = val
|
||||
if (val) {
|
||||
// 恢复上次登录的账号
|
||||
this.username = uni.getStorageSync('XINLI_LAST_USERNAME') || ''
|
||||
// 清空错误和成功信息
|
||||
this.errorMsg = ''
|
||||
this.successMsg = ''
|
||||
// 延迟自动聚焦
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
console.log('Setting autoFocus to true')
|
||||
this.autoFocus = true
|
||||
}, 600)
|
||||
})
|
||||
} else {
|
||||
this.autoFocus = false
|
||||
this.passwordFocus = false
|
||||
this.errorMsg = ''
|
||||
this.successMsg = ''
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onUsernameFocus() {
|
||||
console.log('Username input focused')
|
||||
this.errorMsg = ''
|
||||
this.successMsg = ''
|
||||
},
|
||||
onUsernameBlur() {
|
||||
console.log('Username input blurred')
|
||||
},
|
||||
onPasswordFocus() {
|
||||
console.log('Password input focused')
|
||||
this.errorMsg = ''
|
||||
this.successMsg = ''
|
||||
},
|
||||
onPasswordBlur() {
|
||||
console.log('Password input blurred')
|
||||
},
|
||||
focusPassword() {
|
||||
console.log('Focusing password field')
|
||||
this.autoFocus = false
|
||||
this.$nextTick(() => {
|
||||
this.passwordFocus = true
|
||||
})
|
||||
},
|
||||
onMaskClick() {
|
||||
console.log('Mask clicked, closable:', this.closable)
|
||||
if (this.closable) {
|
||||
this.onClose()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '请先登录后才能使用',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
onClose() {
|
||||
if (!this.closable) {
|
||||
uni.showToast({
|
||||
title: '请先登录后才能使用',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.$emit('close')
|
||||
},
|
||||
onLogin() {
|
||||
console.log('Login button clicked')
|
||||
if (this.loading) return
|
||||
|
||||
const username = (this.username || '').trim()
|
||||
const password = (this.password || '').trim()
|
||||
|
||||
if (!username) {
|
||||
this.errorMsg = '请输入账号'
|
||||
return
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
this.errorMsg = '请输入密码'
|
||||
return
|
||||
}
|
||||
|
||||
this.errorMsg = ''
|
||||
this.successMsg = ''
|
||||
this.loading = true
|
||||
uni.showLoading({ title: '登录中...' })
|
||||
|
||||
login({ username, password })
|
||||
.then((res) => {
|
||||
this.loading = false
|
||||
uni.hideLoading()
|
||||
|
||||
const data = res && res.data ? res.data : null
|
||||
if (!data || data.code !== 200) {
|
||||
const errorMsg = (data && data.msg) ? data.msg : '登录失败'
|
||||
this.errorMsg = errorMsg
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const token = data.token
|
||||
if (!token) {
|
||||
this.errorMsg = '登录失败:未返回token'
|
||||
uni.showToast({
|
||||
title: '登录失败:未返回token',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setToken(token)
|
||||
uni.setStorageSync('XINLI_LAST_USERNAME', username)
|
||||
|
||||
// 显示成功提示
|
||||
this.successMsg = '登录成功!正在进入系统...'
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 清空密码和错误信息
|
||||
this.password = ''
|
||||
this.errorMsg = ''
|
||||
|
||||
// 通知父组件登录成功
|
||||
this.$emit('success')
|
||||
|
||||
// 延迟关闭弹窗并刷新页面
|
||||
setTimeout(() => {
|
||||
this.$emit('close')
|
||||
// 刷新当前页面
|
||||
const pages = getCurrentPages()
|
||||
const currentPage = pages[pages.length - 1]
|
||||
if (currentPage && currentPage.$vm && typeof currentPage.$vm.onShow === 'function') {
|
||||
currentPage.$vm.onShow()
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
.catch((e) => {
|
||||
this.loading = false
|
||||
uni.hideLoading()
|
||||
const errorMsg = e && e.message ? e.message : '网络错误,请检查网络连接'
|
||||
this.errorMsg = errorMsg
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 不使用scoped,确保样式优先级 */
|
||||
.login-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 9999999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-modal-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.login-modal-wrapper {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 600rpx;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.login-modal-content {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.login-modal-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.login-modal-close {
|
||||
font-size: 48rpx;
|
||||
color: #8f959e;
|
||||
line-height: 1;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.login-modal-body {
|
||||
padding: 32rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.login-form-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.login-form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.login-form-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 88rpx;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid transparent;
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
.login-form-input::placeholder {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.login-error-msg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 20rpx;
|
||||
background: #fff2e8;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 16rpx;
|
||||
border-left: 4rpx solid #ff7875;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 26rpx;
|
||||
color: #d4380d;
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.login-success-msg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 20rpx;
|
||||
background: #f6ffed;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 16rpx;
|
||||
border-left: 4rpx solid #52c41a;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
color: #52c41a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.success-text {
|
||||
font-size: 26rpx;
|
||||
color: #389e0d;
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.login-modal-btn {
|
||||
width: 100%;
|
||||
margin-top: 16rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
}
|
||||
|
||||
.login-modal-footer {
|
||||
margin-top: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
font-size: 24rpx;
|
||||
color: #8f959e;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
<template>
|
||||
<view class="tabbar" :class="{ h5: isH5 }" :style="safeAreaStyle">
|
||||
<view class="tabbar-item" :class="{ active: active === 'pages/index/index' }" @tap="switchTab('pages/index/index')">
|
||||
<view class="tabbar-icon">
|
||||
<uni-icons type="home" size="20" :color="active === 'pages/index/index' ? selectedColor : color"></uni-icons>
|
||||
</view>
|
||||
<view class="tabbar-text">面板</view>
|
||||
</view>
|
||||
<view class="tabbar-item" :class="{ active: active === 'pages/message/notice' }" @tap="switchTab('pages/message/notice')">
|
||||
<view class="tabbar-icon">
|
||||
<uni-icons type="notification" size="20" :color="active === 'pages/message/notice' ? selectedColor : color"></uni-icons>
|
||||
</view>
|
||||
<view class="tabbar-text">通知</view>
|
||||
</view>
|
||||
<view class="tabbar-item" :class="{ active: active === 'pages/settings/index' }" @tap="switchTab('pages/settings/index')">
|
||||
<view class="tabbar-icon">
|
||||
<uni-icons type="person" size="20" :color="active === 'pages/settings/index' ? selectedColor : color"></uni-icons>
|
||||
</view>
|
||||
<view class="tabbar-text">我的</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UniIcons
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
active: 'pages/index/index',
|
||||
isH5: false,
|
||||
color: '#646a73',
|
||||
selectedColor: '#1677ff'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
safeAreaStyle() {
|
||||
try {
|
||||
const info = uni.getSystemInfoSync()
|
||||
const bottom = info && info.safeAreaInsets ? info.safeAreaInsets.bottom : 0
|
||||
return bottom ? `padding-bottom:${bottom}px;` : ''
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
try {
|
||||
const info = uni.getSystemInfoSync()
|
||||
this.isH5 = info && info.uniPlatform === 'web'
|
||||
} catch (e) {
|
||||
this.isH5 = false
|
||||
}
|
||||
if (this.isH5) {
|
||||
this.color = 'rgba(201, 242, 255, 0.85)'
|
||||
this.selectedColor = '#74d8ff'
|
||||
}
|
||||
this.updateActive()
|
||||
},
|
||||
onShow() {
|
||||
this.updateActive()
|
||||
},
|
||||
methods: {
|
||||
updateActive() {
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
const current = pages && pages.length ? pages[pages.length - 1] : null
|
||||
let route = current && current.route ? current.route : ''
|
||||
if (route && route[0] === '/') route = route.slice(1)
|
||||
this.active = route || this.active
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
switchTab(url) {
|
||||
this.updateActive()
|
||||
if (this.active === url) return
|
||||
this.active = url
|
||||
uni.switchTab({ url: '/' + url })
|
||||
setTimeout(() => {
|
||||
this.updateActive()
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tabbar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 56px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
box-sizing: content-box;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.tabbar.h5 {
|
||||
background: linear-gradient(180deg, rgba(2, 8, 22, 0.92) 0%, rgba(2, 8, 22, 0.78) 100%);
|
||||
border-top: 1px solid rgba(0, 188, 255, 0.22);
|
||||
box-shadow: 0 -10px 18px rgba(0, 0, 0, 0.35), 0 0 22px rgba(0, 166, 255, 0.10);
|
||||
}
|
||||
|
||||
.tabbar-item {
|
||||
flex: 1;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #646a73;
|
||||
}
|
||||
|
||||
.tabbar.h5 .tabbar-item {
|
||||
color: rgba(201, 242, 255, 0.85);
|
||||
}
|
||||
|
||||
.tabbar-item.active {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.tabbar.h5 .tabbar-item.active {
|
||||
color: #74d8ff;
|
||||
}
|
||||
|
||||
.tabbar-icon {
|
||||
width: 34px;
|
||||
height: 28px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 120ms ease;
|
||||
}
|
||||
|
||||
.tabbar-item.active .tabbar-icon {
|
||||
background: rgba(22, 119, 255, 0.14);
|
||||
}
|
||||
|
||||
.tabbar.h5 .tabbar-item.active .tabbar-icon {
|
||||
background: rgba(116, 216, 255, 0.14);
|
||||
box-shadow: 0 0 14px rgba(116, 216, 255, 0.22);
|
||||
}
|
||||
|
||||
.tabbar-item.active .tabbar-text {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.tabbar.h5 .tabbar-item.active .tabbar-text {
|
||||
color: #74d8ff;
|
||||
}
|
||||
|
||||
.tabbar-text {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
</style>
|
||||
6
xinlidsj/package-lock.json
generated
Normal file
6
xinlidsj/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "xinlidsj",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
|
|
@ -190,29 +190,11 @@
|
|||
"backgroundColor": "#050b18"
|
||||
},
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"color": "#646a73",
|
||||
"selectedColor": "#1677ff",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "black",
|
||||
"custom": false,
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "面板",
|
||||
"iconPath": "static/home.png",
|
||||
"selectedIconPath": "static/home.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/message/notice",
|
||||
"text": "通知",
|
||||
"iconPath": "static/notice.png",
|
||||
"selectedIconPath": "static/notice.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/settings/index",
|
||||
"text": "我的",
|
||||
"iconPath": "static/my_light.png",
|
||||
"selectedIconPath": "static/my_light.png"
|
||||
"text": "首页"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -982,9 +982,9 @@
|
|||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6rpx;
|
||||
top: 10rpx;
|
||||
width: 6rpx;
|
||||
height: 24rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 6rpx;
|
||||
background: linear-gradient(180deg, rgba(116, 216, 255, 0.95) 0%, rgba(43, 107, 255, 0.85) 100%);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<view class="page" v-if="!isH5">
|
||||
<!-- 主内容区域 - 登录时禁用交互 -->
|
||||
<view class="page" v-if="!isH5" :class="{ 'page-disabled': showLoginModal }">
|
||||
<view class="section">
|
||||
<view class="section-title">核心功能</view>
|
||||
<view class="grid">
|
||||
|
|
@ -124,6 +125,9 @@
|
|||
<view class="big-top-action-item" @tap="goInterventionTasks">
|
||||
<image class="big-top-action-ico" src="/static/6.png" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="big-top-action-item" @tap="goNotice">
|
||||
<image class="big-top-action-ico" src="/static/7.png" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -231,6 +235,13 @@
|
|||
<view :id="aiChatBottomId" class="big-ai-bottom"></view>
|
||||
</scroll-view>
|
||||
<view class="big-ai-input">
|
||||
<view
|
||||
class="big-ai-voice-btn"
|
||||
:class="{ active: voiceMode }"
|
||||
@tap="toggleVoiceMode"
|
||||
>
|
||||
<uni-icons type="mic-filled" size="20" :color="voiceMode ? '#00f0ff' : '#64748B'"></uni-icons>
|
||||
</view>
|
||||
<input
|
||||
class="big-ai-text"
|
||||
v-model="aiChatInput"
|
||||
|
|
@ -307,6 +318,9 @@
|
|||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 登录弹窗 - 放在最后确保在最上层 -->
|
||||
<LoginModal :show="showLoginModal" :closable="false" @success="onLoginSuccess" @close="showLoginModal = false" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -315,21 +329,30 @@
|
|||
import { getUnreadMessageList, markMessageRead } from '../../api/app/message'
|
||||
import { openLink, getMessageWsUrl } from '../../utils/link'
|
||||
import { request } from '../../utils/request'
|
||||
import { getBaseUrl } from '../../utils/config'
|
||||
import { getReport, listReport } from '../../api/psychology/report'
|
||||
import { getStudentOptions, getUserAssessmentSummary } from '../../api/psychology/assessment'
|
||||
import QiunDataCharts from '../../uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
|
||||
import UniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
|
||||
import LoginModal from '../../components/LoginModal.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
QiunDataCharts,
|
||||
UniIcons
|
||||
UniIcons,
|
||||
LoginModal
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showLoginModal: false,
|
||||
socketOpen: false,
|
||||
connecting: false,
|
||||
voiceTipsOpen: false,
|
||||
voiceMode: false,
|
||||
recorder: null,
|
||||
mediaRecorder: null,
|
||||
mediaStream: null,
|
||||
audioChunks: [],
|
||||
isH5: false,
|
||||
centerVideoUrl: '',
|
||||
aiChatOpen: true,
|
||||
|
|
@ -527,11 +550,32 @@
|
|||
} catch (e) {
|
||||
this.isH5 = false
|
||||
}
|
||||
// 检查登录状态
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
this.showLoginModal = true
|
||||
return
|
||||
}
|
||||
this.initApp()
|
||||
},
|
||||
onShow() {
|
||||
// 检查登录状态
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
this.showLoginModal = true
|
||||
return
|
||||
}
|
||||
this.checkUnreadNoticePopup()
|
||||
this.checkUnreadMessagePopup()
|
||||
if (this.isH5) {
|
||||
this.fetchInboxList()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initApp() {
|
||||
if (this.isH5) {
|
||||
this.fetchCenterVideo()
|
||||
}
|
||||
const token = getToken()
|
||||
if (!token) return
|
||||
this.initMessageChannel()
|
||||
this.checkUnreadNoticePopup()
|
||||
this.checkUnreadMessagePopup()
|
||||
|
|
@ -540,16 +584,10 @@
|
|||
this.fetchInboxList()
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
const token = getToken()
|
||||
if (!token) return
|
||||
this.checkUnreadNoticePopup()
|
||||
this.checkUnreadMessagePopup()
|
||||
if (this.isH5) {
|
||||
this.fetchInboxList()
|
||||
}
|
||||
onLoginSuccess() {
|
||||
// 登录成功后初始化应用
|
||||
this.initApp()
|
||||
},
|
||||
methods: {
|
||||
getBailianConfig() {
|
||||
const HARDCODED_BAILIAN_API_KEY = 'sk-f991fd13fb044abebeaea81b9848c22b'
|
||||
let env = null
|
||||
|
|
@ -716,6 +754,255 @@
|
|||
})
|
||||
}
|
||||
},
|
||||
toggleVoiceMode() {
|
||||
this.voiceMode = !this.voiceMode
|
||||
if (this.voiceMode) {
|
||||
uni.showToast({
|
||||
title: '语音对话已开启',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
this.startVoiceRecognition()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '语音对话已关闭',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
this.stopVoiceRecognition()
|
||||
}
|
||||
},
|
||||
startVoiceRecognition() {
|
||||
// H5环境下使用浏览器原生录音API
|
||||
// #ifdef H5
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
uni.showToast({
|
||||
title: '浏览器不支持录音功能',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
this.voiceMode = false
|
||||
return
|
||||
}
|
||||
|
||||
navigator.mediaDevices.getUserMedia({ audio: true })
|
||||
.then(stream => {
|
||||
console.log('获取麦克风权限成功')
|
||||
this.mediaStream = stream
|
||||
this.mediaRecorder = new MediaRecorder(stream)
|
||||
this.audioChunks = []
|
||||
|
||||
this.mediaRecorder.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
this.audioChunks.push(event.data)
|
||||
}
|
||||
}
|
||||
|
||||
this.mediaRecorder.onstop = () => {
|
||||
console.log('录音停止')
|
||||
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' })
|
||||
this.uploadAudioBlob(audioBlob)
|
||||
|
||||
// 停止所有音轨
|
||||
if (this.mediaStream) {
|
||||
this.mediaStream.getTracks().forEach(track => track.stop())
|
||||
}
|
||||
}
|
||||
|
||||
this.mediaRecorder.start()
|
||||
console.log('开始录音...')
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('获取麦克风权限失败:', err)
|
||||
uni.showToast({
|
||||
title: '无法访问麦克风,请检查权限',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
this.voiceMode = false
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
// 检查是否支持录音
|
||||
if (typeof uni.getRecorderManager !== 'function') {
|
||||
uni.showToast({
|
||||
title: '当前环境不支持录音功能',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
this.voiceMode = false
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.recorder) {
|
||||
this.recorder = uni.getRecorderManager()
|
||||
this.recorder.onStop((res) => {
|
||||
console.log('录音停止', res)
|
||||
this.onVoiceRecorderStop(res)
|
||||
})
|
||||
this.recorder.onError((err) => {
|
||||
console.error('录音错误', err)
|
||||
this.voiceMode = false
|
||||
const msg = (err && err.errMsg) ? err.errMsg : '录音失败'
|
||||
uni.showToast({
|
||||
title: msg,
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
this.recorder.onStart(() => {
|
||||
console.log('录音开始')
|
||||
})
|
||||
}
|
||||
|
||||
console.log('开始录音...')
|
||||
this.recorder.start({
|
||||
duration: 60000,
|
||||
sampleRate: 16000,
|
||||
numberOfChannels: 1,
|
||||
encodeBitRate: 96000,
|
||||
format: 'mp3'
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
stopVoiceRecognition() {
|
||||
// #ifdef H5
|
||||
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
|
||||
this.mediaRecorder.stop()
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef H5
|
||||
if (this.recorder && this.voiceMode) {
|
||||
this.recorder.stop()
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
onVoiceRecorderStop(res) {
|
||||
if (!res || !res.tempFilePath) {
|
||||
uni.showToast({
|
||||
title: '未获取到录音文件',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.uploadAndAsrForAiChat(res.tempFilePath)
|
||||
},
|
||||
uploadAndAsrForAiChat(tempFilePath) {
|
||||
console.log('开始上传音频文件:', tempFilePath)
|
||||
uni.showLoading({ title: '识别中...' })
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
console.log('上传地址:', baseUrl + '/voice/asr')
|
||||
|
||||
uni.uploadFile({
|
||||
url: baseUrl + '/voice/asr',
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
formData: { language: 'zh' },
|
||||
header: {
|
||||
'Authorization': getToken() || ''
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('上传成功,响应:', res)
|
||||
uni.hideLoading()
|
||||
let data = null
|
||||
try {
|
||||
data = JSON.parse(res.data)
|
||||
} catch (e) {
|
||||
console.error('解析响应失败:', e)
|
||||
uni.showToast({
|
||||
title: '服务返回格式错误',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
console.log('解析后的数据:', data)
|
||||
|
||||
if (!data || data.code !== 200) {
|
||||
uni.showToast({
|
||||
title: (data && data.msg) ? data.msg : '识别失败',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const recognizedText = data.text || ''
|
||||
if (!recognizedText) {
|
||||
uni.showToast({
|
||||
title: '未识别到有效文本',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
console.log('识别文本:', recognizedText)
|
||||
// 将识别的文本设置到输入框并自动发送
|
||||
this.aiChatInput = recognizedText
|
||||
this.sendAiChat()
|
||||
},
|
||||
fail: (e) => {
|
||||
console.error('上传失败:', e)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: e && e.errMsg ? e.errMsg : '上传失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
uploadAudioBlob(audioBlob) {
|
||||
console.log('开始上传音频Blob:', audioBlob)
|
||||
uni.showLoading({ title: '识别中...' })
|
||||
|
||||
const baseUrl = getBaseUrl()
|
||||
const formData = new FormData()
|
||||
formData.append('file', audioBlob, 'audio.webm')
|
||||
formData.append('language', 'zh')
|
||||
|
||||
fetch(baseUrl + '/voice/asr', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': getToken() || ''
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('上传成功,响应:', data)
|
||||
uni.hideLoading()
|
||||
|
||||
if (!data || data.code !== 200) {
|
||||
uni.showToast({
|
||||
title: (data && data.msg) ? data.msg : '识别失败',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const recognizedText = data.text || ''
|
||||
if (!recognizedText) {
|
||||
uni.showToast({
|
||||
title: '未识别到有效文本',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
console.log('识别文本:', recognizedText)
|
||||
this.aiChatInput = recognizedText
|
||||
this.sendAiChat()
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('上传失败:', err)
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '上传失败: ' + err.message,
|
||||
icon: 'none'
|
||||
})
|
||||
})
|
||||
},
|
||||
scrollAiChatToBottom() {
|
||||
// scroll-into-view 需要一个变化的值触发
|
||||
this.aiChatScrollInto = this.aiChatBottomId + '-' + Date.now()
|
||||
|
|
@ -1627,8 +1914,13 @@
|
|||
url: '/pages/dashboard/index'
|
||||
})
|
||||
},
|
||||
goNotice() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/message/notice'
|
||||
})
|
||||
},
|
||||
goSettings() {
|
||||
uni.switchTab({
|
||||
uni.navigateTo({
|
||||
url: '/pages/settings/index'
|
||||
})
|
||||
}
|
||||
|
|
@ -1637,20 +1929,41 @@
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自适应字体大小变量 - 基于视口宽度 */
|
||||
:root {
|
||||
--font-xs: calc(22rpx + 0.3vw); /* 小字体 */
|
||||
--font-sm: calc(24rpx + 0.35vw); /* 小字体 */
|
||||
--font-base: calc(26rpx + 0.4vw); /* 基础字体 */
|
||||
--font-md: calc(28rpx + 0.45vw); /* 中等字体 */
|
||||
--font-lg: calc(32rpx + 0.6vw); /* 大字体 */
|
||||
--font-xl: calc(38rpx + 0.75vw); /* 超大字体 */
|
||||
--font-2xl: calc(56rpx + 1.2vw); /* 特大字体 */
|
||||
--font-3xl: calc(60rpx + 1.5vw); /* 巨大字体 */
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 24rpx 24rpx 120rpx;
|
||||
padding: 24rpx 24rpx 0;
|
||||
box-sizing: border-box;
|
||||
background: #F4F6FB;
|
||||
--c-primary: #1677ff;
|
||||
--c-danger: #E87A7A;
|
||||
/* 页面级字体变量 */
|
||||
--font-xs: calc(22rpx + 0.3vw);
|
||||
--font-sm: calc(24rpx + 0.35vw);
|
||||
--font-base: calc(26rpx + 0.4vw);
|
||||
--font-md: calc(28rpx + 0.45vw);
|
||||
--font-lg: calc(32rpx + 0.6vw);
|
||||
--font-xl: calc(38rpx + 0.75vw);
|
||||
--font-2xl: calc(56rpx + 1.2vw);
|
||||
--font-3xl: calc(60rpx + 1.5vw);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-size: var(--font-md);
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin: 10rpx 6rpx 14rpx;
|
||||
|
|
@ -1739,8 +2052,8 @@
|
|||
}
|
||||
|
||||
.card-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
width: calc(64rpx + 1.5vh);
|
||||
height: calc(64rpx + 1.5vh);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
display: flex;
|
||||
|
|
@ -1777,14 +2090,14 @@
|
|||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-size: var(--font-lg);
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
|
|
@ -1802,7 +2115,7 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
.fold-title {
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
}
|
||||
|
|
@ -1810,7 +2123,7 @@
|
|||
padding: 0 14rpx 14rpx;
|
||||
}
|
||||
.fold-item {
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
color: #475569;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
|
@ -1838,7 +2151,7 @@
|
|||
transform: scale(0.98);
|
||||
}
|
||||
.mini-text {
|
||||
font-size: 22rpx;
|
||||
font-size: var(--font-xs);
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
|
|
@ -1847,8 +2160,8 @@
|
|||
}
|
||||
|
||||
.page.big {
|
||||
min-height: 100vh;
|
||||
padding: 18rpx 18rpx 24rpx;
|
||||
height: 100vh;
|
||||
padding: 12rpx 12rpx 0;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
|
@ -1859,14 +2172,14 @@
|
|||
--glass-2: rgba(10, 18, 38, 0.52);
|
||||
background: #020610;
|
||||
color: rgba(242, 252, 255, 0.96);
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
line-height: 1.5;
|
||||
text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.big-center-media {
|
||||
width: 100%;
|
||||
height: calc(74vh - 360rpx);
|
||||
max-height: 720rpx;
|
||||
max-height: calc(720rpx + 7vh);
|
||||
border-radius: 18rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -1898,7 +2211,7 @@
|
|||
padding: 16rpx 18rpx;
|
||||
}
|
||||
.big-center-text-title {
|
||||
font-size: 26rpx;
|
||||
font-size: var(--font-base);
|
||||
font-weight: 900;
|
||||
color: rgba(220, 250, 255, 0.94);
|
||||
letter-spacing: 1rpx;
|
||||
|
|
@ -1906,16 +2219,16 @@
|
|||
}
|
||||
.big-center-text-desc {
|
||||
margin-top: 10rpx;
|
||||
font-size: 22rpx;
|
||||
font-size: var(--font-xs);
|
||||
line-height: 32rpx;
|
||||
color: rgba(201, 242, 255, 0.72);
|
||||
}
|
||||
.big-panel-inbox {
|
||||
margin-top: 16rpx;
|
||||
min-height: 320rpx;
|
||||
min-height: calc(320rpx + 2vh);
|
||||
}
|
||||
.big-panel-portrait .big-chart {
|
||||
height: 420rpx;
|
||||
height: calc(420rpx + 3vh);
|
||||
}
|
||||
.big-inbox-list {
|
||||
display: flex;
|
||||
|
|
@ -1930,16 +2243,16 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
.big-inbox-title {
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 900;
|
||||
color: rgba(242, 252, 255, 0.92);
|
||||
}
|
||||
.big-inbox-desc {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
font-size: var(--font-xs);
|
||||
color: rgba(242, 252, 255, 0.84);
|
||||
line-height: 28rpx;
|
||||
max-height: 56rpx;
|
||||
max-height: calc(56rpx + 0.5vh);
|
||||
overflow: hidden;
|
||||
}
|
||||
.page.big:before {
|
||||
|
|
@ -1996,11 +2309,11 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 60rpx;
|
||||
font-size: var(--font-3xl);
|
||||
font-weight: 900;
|
||||
letter-spacing: 2rpx;
|
||||
color: rgba(242, 252, 255, 0.98);
|
||||
min-height: 120rpx;
|
||||
min-height: calc(120rpx + 1.2vh);
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
|
|
@ -2025,9 +2338,10 @@
|
|||
z-index: 3;
|
||||
padding: 0 40rpx;
|
||||
box-sizing: border-box;
|
||||
font-size: calc(48rpx + 0.9vw);
|
||||
}
|
||||
.big-title {
|
||||
min-height: 120rpx;
|
||||
min-height: calc(120rpx + 1.2vh);
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
|
@ -2048,12 +2362,12 @@
|
|||
}
|
||||
.big-top-action-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 12rpx;
|
||||
align-items: center;
|
||||
}
|
||||
.big-top-action-item {
|
||||
height: 140rpx;
|
||||
height: calc(140rpx + 1.5vh);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -2063,14 +2377,14 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
.big-top-action-ico {
|
||||
width: 124rpx;
|
||||
height: 124rpx;
|
||||
width: calc(124rpx + 1.2vh);
|
||||
height: calc(124rpx + 1.2vh);
|
||||
filter: drop-shadow(0 0 14rpx rgba(0, 240, 255, 0.26)) drop-shadow(0 0 26rpx rgba(60, 140, 255, 0.18));
|
||||
}
|
||||
.big-grid {
|
||||
margin-top: 14rpx;
|
||||
display: flex;
|
||||
gap: 22rpx;
|
||||
gap: 12rpx;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin-left: 0;
|
||||
|
|
@ -2078,14 +2392,14 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
.big-col {
|
||||
width: 28%;
|
||||
width: 27%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.big-center {
|
||||
flex: 0 0 44%;
|
||||
width: 44%;
|
||||
flex: 0 0 46%;
|
||||
width: 46%;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -2094,8 +2408,8 @@
|
|||
.big-panel {
|
||||
position: relative;
|
||||
border-radius: 18rpx;
|
||||
padding: 26rpx 26rpx 22rpx;
|
||||
min-height: 320rpx;
|
||||
padding: 18rpx 18rpx 16rpx;
|
||||
min-height: calc(320rpx + 2vh);
|
||||
border: 1px solid rgba(0, 240, 255, 0.16);
|
||||
background-image:
|
||||
linear-gradient(135deg, rgba(0, 240, 255, 0.08) 0%, rgba(60, 140, 255, 0.05) 35%, rgba(165, 90, 255, 0.06) 100%),
|
||||
|
|
@ -2107,18 +2421,18 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
.big-panel-core {
|
||||
min-height: 460rpx;
|
||||
min-height: calc(460rpx + 3vh);
|
||||
}
|
||||
.big-panel-core .big-ring {
|
||||
height: 440rpx;
|
||||
height: calc(440rpx + 3vh);
|
||||
}
|
||||
.big-panel-core .big-ring:before {
|
||||
width: 440rpx;
|
||||
height: 440rpx;
|
||||
width: calc(440rpx + 3vh);
|
||||
height: calc(440rpx + 3vh);
|
||||
}
|
||||
.big-panel-core .big-ring-center {
|
||||
width: 250rpx;
|
||||
height: 250rpx;
|
||||
width: calc(250rpx + 2vh);
|
||||
height: calc(250rpx + 2vh);
|
||||
}
|
||||
.big-metrics {
|
||||
position: relative;
|
||||
|
|
@ -2134,7 +2448,7 @@
|
|||
min-width: 340rpx;
|
||||
}
|
||||
.big-metric-label {
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 700;
|
||||
color: rgba(220, 250, 255, 0.78);
|
||||
letter-spacing: 1rpx;
|
||||
|
|
@ -2142,7 +2456,7 @@
|
|||
}
|
||||
.big-metric-value {
|
||||
display: block;
|
||||
font-size: 56rpx;
|
||||
font-size: var(--font-2xl);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
color: rgba(242, 252, 255, 0.96);
|
||||
|
|
@ -2218,7 +2532,7 @@
|
|||
opacity: 0.95;
|
||||
}
|
||||
.big-panel-title {
|
||||
font-size: 32rpx;
|
||||
font-size: var(--font-lg);
|
||||
font-weight: 900;
|
||||
color: rgba(242, 252, 255, 0.94);
|
||||
padding-left: 14rpx;
|
||||
|
|
@ -2259,8 +2573,9 @@
|
|||
}
|
||||
.big-panel-report .big-panel-title:before {
|
||||
left: 10rpx;
|
||||
top: 10rpx;
|
||||
height: 22rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: calc(22rpx + 0.8vh);
|
||||
background: linear-gradient(180deg, rgba(0, 240, 255, 0.95) 0%, rgba(39, 120, 255, 0.85) 55%, rgba(165, 90, 255, 0.85) 100%);
|
||||
box-shadow: 0 0 14rpx rgba(0, 240, 255, 0.18);
|
||||
}
|
||||
|
|
@ -2282,14 +2597,15 @@
|
|||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 6rpx;
|
||||
height: 24rpx;
|
||||
height: calc(24rpx + 0.8vh);
|
||||
border-radius: 6rpx;
|
||||
background: linear-gradient(180deg, rgba(116, 216, 255, 0.95) 0%, rgba(43, 107, 255, 0.85) 100%);
|
||||
}
|
||||
.big-panel-sub {
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
color: rgba(242, 252, 255, 0.84);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
|
@ -2307,7 +2623,7 @@
|
|||
.big-panel-clear {
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
font-size: 22rpx;
|
||||
font-size: var(--font-xs);
|
||||
font-weight: 900;
|
||||
color: rgba(242, 252, 255, 0.86);
|
||||
border: 1px solid rgba(116, 216, 255, 0.20);
|
||||
|
|
@ -2331,7 +2647,7 @@
|
|||
z-index: 1;
|
||||
}
|
||||
.big-ai-messages {
|
||||
height: 640rpx;
|
||||
height: calc(640rpx + 4vh);
|
||||
padding: 8rpx 10rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 14rpx;
|
||||
|
|
@ -2340,7 +2656,7 @@
|
|||
}
|
||||
.big-ai-empty {
|
||||
padding: 16rpx 10rpx;
|
||||
font-size: 22rpx;
|
||||
font-size: var(--font-xs);
|
||||
color: rgba(242, 252, 255, 0.62);
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
|
|
@ -2359,7 +2675,7 @@
|
|||
max-width: 78%;
|
||||
padding: 12rpx 14rpx;
|
||||
border-radius: 14rpx;
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
line-height: 1.45;
|
||||
color: rgba(242, 252, 255, 0.92);
|
||||
border: 1px solid rgba(0, 240, 255, 0.16);
|
||||
|
|
@ -2380,25 +2696,41 @@
|
|||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
.big-ai-voice-btn {
|
||||
width: calc(72rpx + 2vh);
|
||||
height: calc(72rpx + 2vh);
|
||||
border-radius: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(116, 216, 255, 0.18);
|
||||
background: rgba(7, 13, 28, 0.78);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.big-ai-voice-btn.active {
|
||||
border-color: rgba(0, 240, 255, 0.6);
|
||||
background: rgba(0, 240, 255, 0.12);
|
||||
box-shadow: 0 0 20rpx rgba(0, 240, 255, 0.4);
|
||||
}
|
||||
.big-ai-text {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
height: calc(72rpx + 2vh);
|
||||
border-radius: 14rpx;
|
||||
border: 1px solid rgba(116, 216, 255, 0.18);
|
||||
background: rgba(7, 13, 28, 0.78);
|
||||
padding: 0 14rpx;
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
color: rgba(242, 252, 255, 0.92);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.big-ai-send {
|
||||
width: 120rpx;
|
||||
height: 72rpx;
|
||||
height: calc(72rpx + 2vh);
|
||||
border-radius: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 900;
|
||||
color: rgba(242, 252, 255, 0.92);
|
||||
border: 1px solid rgba(0, 240, 255, 0.22);
|
||||
|
|
@ -2411,7 +2743,7 @@
|
|||
.big-ring {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 360rpx;
|
||||
height: calc(360rpx + 2.5vh);
|
||||
}
|
||||
.big-ring:before {
|
||||
content: '';
|
||||
|
|
@ -2419,8 +2751,8 @@
|
|||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
width: calc(360rpx + 2.5vh);
|
||||
height: calc(360rpx + 2.5vh);
|
||||
border-radius: 999rpx;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
|
|
@ -2441,8 +2773,8 @@
|
|||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 210rpx;
|
||||
height: 210rpx;
|
||||
width: calc(210rpx + 5vh);
|
||||
height: calc(210rpx + 5vh);
|
||||
border-radius: 999rpx;
|
||||
background: rgba(7, 13, 28, 0.72);
|
||||
z-index: 5;
|
||||
|
|
@ -2454,12 +2786,12 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
.big-ring-center-main {
|
||||
font-size: 34rpx;
|
||||
font-size: var(--font-lg);
|
||||
font-weight: 900;
|
||||
color: rgba(242, 252, 255, 0.96);
|
||||
}
|
||||
.big-ring-center-sub {
|
||||
font-size: 22rpx;
|
||||
font-size: var(--font-xs);
|
||||
color: rgba(242, 252, 255, 0.84);
|
||||
text-align: center;
|
||||
line-height: 26rpx;
|
||||
|
|
@ -2468,7 +2800,7 @@
|
|||
.big-chart {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 340rpx;
|
||||
height: calc(340rpx + 2.5vh);
|
||||
}
|
||||
.big-portrait-cloud {
|
||||
position: relative;
|
||||
|
|
@ -2510,7 +2842,7 @@
|
|||
100% { transform: translate(-50%, -50%) translate(0, 0); }
|
||||
}
|
||||
.big-chart-xl {
|
||||
height: 400rpx;
|
||||
height: calc(400rpx + 3vh);
|
||||
}
|
||||
.big-kpis {
|
||||
position: relative;
|
||||
|
|
@ -2533,19 +2865,19 @@
|
|||
gap: 10rpx;
|
||||
}
|
||||
.big-kpi-ico {
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
width: calc(34rpx + 1vh);
|
||||
height: calc(34rpx + 1vh);
|
||||
filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
|
||||
}
|
||||
.big-kpi-num {
|
||||
font-size: 38rpx;
|
||||
font-size: var(--font-xl);
|
||||
font-weight: 900;
|
||||
color: #74d8ff;
|
||||
text-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
.big-kpi-lab {
|
||||
margin-top: 6rpx;
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
color: rgba(242, 252, 255, 0.84);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
|
@ -2563,12 +2895,12 @@
|
|||
padding: 10rpx 2rpx 2rpx;
|
||||
}
|
||||
.big-panel-actions {
|
||||
min-height: 380rpx;
|
||||
min-height: calc(380rpx + 3vh);
|
||||
}
|
||||
.big-action-item {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 150rpx;
|
||||
height: calc(150rpx + 1.5vh);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
@ -2584,13 +2916,13 @@
|
|||
}
|
||||
.big-action-item:active { transform: scale(0.98); }
|
||||
.big-action-ico {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
width: calc(68rpx + 2vh);
|
||||
height: calc(68rpx + 2vh);
|
||||
flex: 0 0 auto;
|
||||
filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
|
||||
}
|
||||
.big-action-text {
|
||||
font-size: 26rpx;
|
||||
font-size: var(--font-base);
|
||||
font-weight: 900;
|
||||
color: rgba(242, 252, 255, 0.92);
|
||||
line-height: 1.1;
|
||||
|
|
@ -2613,8 +2945,8 @@
|
|||
gap: 10rpx;
|
||||
}
|
||||
.big-tool-icon {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
width: calc(50rpx + 1.5vh);
|
||||
height: calc(50rpx + 1.5vh);
|
||||
border-radius: 12rpx;
|
||||
border: 1px solid rgba(116, 216, 255, 0.22);
|
||||
background: rgba(10, 18, 38, 0.55);
|
||||
|
|
@ -2623,7 +2955,7 @@
|
|||
justify-content: center;
|
||||
}
|
||||
.big-tool-text {
|
||||
font-size: 24rpx;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 900;
|
||||
color: rgba(242, 252, 255, 0.92);
|
||||
}
|
||||
|
|
@ -2642,8 +2974,8 @@
|
|||
padding: 10rpx 0;
|
||||
}
|
||||
.big-hex {
|
||||
width: 96rpx;
|
||||
height: 86rpx;
|
||||
width: calc(96rpx + 3vh);
|
||||
height: calc(86rpx + 2.7vh);
|
||||
clip-path: polygon(25% 6.7%, 75% 6.7%, 100% 50%, 75% 93.3%, 25% 93.3%, 0% 50%);
|
||||
border: 1px solid rgba(116, 216, 255, 0.28);
|
||||
background: radial-gradient(circle at 50% 30%, rgba(116, 216, 255, 0.22) 0%, rgba(10, 18, 38, 0.55) 70%);
|
||||
|
|
@ -2653,7 +2985,7 @@
|
|||
box-shadow: 0 0 18rpx rgba(116, 216, 255, 0.18);
|
||||
}
|
||||
.big-nav-text {
|
||||
font-size: 22rpx;
|
||||
font-size: var(--font-xs);
|
||||
font-weight: 900;
|
||||
color: rgba(242, 252, 255, 0.86);
|
||||
}
|
||||
|
|
@ -2667,4 +2999,10 @@
|
|||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录弹窗显示时禁用页面交互 */
|
||||
.page-disabled {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
<template>
|
||||
<view class="page" :class="{ big: isH5 }">
|
||||
<view v-if="isH5" class="big-top">
|
||||
<view class="big-back" @tap="goBack">
|
||||
<uni-icons type="back" size="24" color="rgba(220, 250, 255, 0.95)"></uni-icons>
|
||||
</view>
|
||||
<view class="big-top-content">
|
||||
<view class="big-title">标签筛选</view>
|
||||
<view class="big-sub">按标签快速定位重点人员</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">筛选条件</view>
|
||||
|
|
@ -81,6 +86,11 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
uni.navigateBack({
|
||||
delta: 1
|
||||
})
|
||||
},
|
||||
fetchTagOptions() {
|
||||
this.tagLoading = true
|
||||
const defaultOptions = [
|
||||
|
|
@ -208,11 +218,36 @@
|
|||
background: linear-gradient(90deg, rgba(2, 8, 22, 0.86) 0%, rgba(2, 8, 22, 0.40) 50%, rgba(2, 8, 22, 0.86) 100%);
|
||||
box-shadow: 0 10rpx 22rpx rgba(0, 0, 0, 0.35), 0 0 24rpx rgba(0, 166, 255, 0.14);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
margin-bottom: 18rpx;
|
||||
position: relative;
|
||||
}
|
||||
.page.big .big-back {
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(0, 188, 255, 0.12);
|
||||
border: 1px solid rgba(0, 188, 255, 0.22);
|
||||
cursor: pointer;
|
||||
}
|
||||
.page.big .big-back:active {
|
||||
background: rgba(0, 188, 255, 0.20);
|
||||
}
|
||||
.page.big .big-top-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
}
|
||||
.page.big .big-title {
|
||||
font-size: 34rpx;
|
||||
|
|
|
|||
BIN
xinlidsj/static/7.png
Normal file
BIN
xinlidsj/static/7.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
|
|
@ -11,3 +11,4 @@ export function setToken(token) {
|
|||
export function clearToken() {
|
||||
uni.removeStorageSync(STORAGE_KEYS.token)
|
||||
}
|
||||
|
||||
|
|
|
|||
106
xinlidsj/登录弹窗实现说明.md
Normal file
106
xinlidsj/登录弹窗实现说明.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# 登录弹窗实现说明
|
||||
|
||||
## 功能概述
|
||||
实现了用户访问系统时,如果未登录则自动弹出登录弹窗,要求用户登录后才能使用系统功能。
|
||||
|
||||
## 实现方式
|
||||
|
||||
### 1. 创建登录弹窗组件
|
||||
**文件**: `xinlidsj/components/LoginModal.vue`
|
||||
|
||||
**功能特点**:
|
||||
- 美观的弹窗UI设计
|
||||
- 支持账号密码登录
|
||||
- 登录状态加载提示
|
||||
- 登录成功后自动关闭弹窗并刷新页面
|
||||
- 可配置是否允许关闭(`closable`属性)
|
||||
- 自动记住上次登录的账号
|
||||
|
||||
**使用方法**:
|
||||
```vue
|
||||
<LoginModal
|
||||
:show="showLoginModal"
|
||||
:closable="false"
|
||||
@success="onLoginSuccess"
|
||||
@close="showLoginModal = false"
|
||||
/>
|
||||
```
|
||||
|
||||
**Props**:
|
||||
- `show`: Boolean - 是否显示弹窗
|
||||
- `closable`: Boolean - 是否允许关闭弹窗(默认false,强制登录)
|
||||
|
||||
**Events**:
|
||||
- `success`: 登录成功时触发
|
||||
- `close`: 关闭弹窗时触发
|
||||
|
||||
### 2. 首页集成登录弹窗
|
||||
**文件**: `xinlidsj/pages/index/index.vue`
|
||||
|
||||
**修改内容**:
|
||||
1. 引入 `LoginModal` 组件
|
||||
2. 添加 `showLoginModal` 数据属性
|
||||
3. 在 `onLoad` 和 `onShow` 生命周期中检查登录状态
|
||||
4. 未登录时显示登录弹窗
|
||||
5. 登录成功后初始化应用数据
|
||||
|
||||
**核心逻辑**:
|
||||
```javascript
|
||||
onLoad() {
|
||||
// 检查登录状态
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
this.showLoginModal = true // 显示登录弹窗
|
||||
return
|
||||
}
|
||||
this.initApp() // 已登录,初始化应用
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 登录成功处理
|
||||
登录成功后的流程:
|
||||
1. 保存token到本地存储
|
||||
2. 触发 `success` 事件
|
||||
3. 父组件调用 `onLoginSuccess()` 方法
|
||||
4. 初始化应用数据(加载视频、消息、数据等)
|
||||
5. 自动关闭登录弹窗
|
||||
6. 刷新当前页面
|
||||
|
||||
## 用户体验
|
||||
|
||||
### 未登录状态
|
||||
- 访问首页时自动弹出登录弹窗
|
||||
- 弹窗遮罩层阻止用户操作页面内容
|
||||
- 不允许关闭弹窗(`closable=false`),必须登录
|
||||
- 点击遮罩层会提示"请先登录后才能使用"
|
||||
|
||||
### 登录过程
|
||||
- 输入账号密码
|
||||
- 点击登录按钮或按回车键提交
|
||||
- 显示"登录中..."加载状态
|
||||
- 登录失败显示错误提示
|
||||
- 登录成功显示成功提示并自动关闭弹窗
|
||||
|
||||
### 已登录状态
|
||||
- 直接进入系统,不显示登录弹窗
|
||||
- 正常使用所有功能
|
||||
|
||||
## 样式特点
|
||||
- 现代化的弹窗设计
|
||||
- 半透明黑色遮罩层
|
||||
- 圆角卡片式弹窗
|
||||
- 输入框聚焦时高亮效果
|
||||
- 响应式布局,适配不同屏幕尺寸
|
||||
|
||||
## 扩展性
|
||||
如果需要在其他页面也使用登录弹窗,只需:
|
||||
1. 引入 `LoginModal` 组件
|
||||
2. 添加 `showLoginModal` 数据属性
|
||||
3. 在页面加载时检查登录状态
|
||||
4. 根据需要设置 `closable` 属性
|
||||
|
||||
## 注意事项
|
||||
1. 登录弹窗使用 `z-index: 9999` 确保在最上层显示
|
||||
2. 弹窗不可关闭时,点击遮罩层会有友好提示
|
||||
3. 登录成功后会自动刷新当前页面数据
|
||||
4. 密码输入框使用 `password` 类型,确保安全性
|
||||
Loading…
Reference in New Issue
Block a user