门店端优化

This commit is contained in:
wong
2025-11-03 14:07:15 +08:00
parent 367a76a54f
commit 469ab4a085
10 changed files with 1326 additions and 189 deletions

View File

@@ -1,14 +1,37 @@
<script> <script>
import { hasValidToken, redirectToLogin } from './api/utils/auth'; import { hasValidToken, redirectToLogin } from './api/utils/auth';
import { appApi } from './api/modules/app';
import UpdateModal from './components/UpdateModal.vue';
export default { export default {
components: {
UpdateModal
},
data() {
return {
// #ifdef APP-PLUS
// 更新弹窗相关数据仅APP端
showUpdateModal: false,
updateInfo: {
version: '',
updateContent: '',
downloadUrl: '',
forceUpdate: false
}
// #endif
};
},
onLaunch: function() { onLaunch: function() {
console.log('App Launch'); console.log('App Launch');
// 检测APP更新
this.checkAppUpdate();
// 全局检查token // 全局检查token
this.checkToken(); this.checkToken();
}, },
onShow: function() { onShow: function() {
console.log('App Show'); console.log('App Show');
// 每次显示时检测APP更新
this.checkAppUpdate();
// 应用恢复时再次检查token // 应用恢复时再次检查token
this.checkToken(); this.checkToken();
}, },
@@ -21,12 +44,69 @@
// 获取当前页面 // 获取当前页面
const pages = getCurrentPages(); const pages = getCurrentPages();
const currentPage = pages.length ? pages[pages.length - 1] : null; const currentPage = pages.length ? pages[pages.length - 1] : null;
const currentRoute = currentPage ? currentPage.route : '';
// 如果token无效且不在登录页面则跳转到登录页面 // 如果token无效且不在登录页面则跳转到登录页面
if (!hasValidToken() && currentPage && currentPage.route !== 'pages/login/index') { if (!hasValidToken() && currentRoute && currentRoute !== 'pages/login/index') {
console.log('Token无效从', currentRoute, '重定向到登录页面');
redirectToLogin(); redirectToLogin();
} }
},
// 检测APP更新
async checkAppUpdate() {
// #ifdef APP-PLUS
try {
console.log('开始检测APP更新...');
const res = await appApi.checkUpdate();
console.log('更新检测结果:', res);
if (res.code === 200 && res.data) {
const data = res.data;
// 清理 downloadUrl 中的换行符
const downloadUrl = data.downloadUrl ? data.downloadUrl.trim() : '';
// 设置更新信息
this.updateInfo = {
version: data.version || '',
updateContent: data.updateContent || '本次更新包含性能优化和问题修复',
downloadUrl: downloadUrl,
forceUpdate: data.forceUpdate || false
};
// 显示更新弹窗
this.showUpdateModal = true;
}
} catch (error) {
console.error('检测更新失败:', error);
// 更新检测失败不影响应用正常使用,只记录日志
} }
// #endif
},
// 处理更新确认
handleUpdateConfirm(downloadUrl) {
// #ifdef APP-PLUS
if (downloadUrl) {
plus.runtime.openURL(downloadUrl);
}
// #endif
this.showUpdateModal = false;
},
// 处理更新取消
handleUpdateCancel() {
if (this.updateInfo.forceUpdate) {
// 强制更新时,取消则退出应用
// #ifdef APP-PLUS
plus.runtime.quit();
// #endif
} else {
// 关闭弹窗
this.showUpdateModal = false;
}
}
} }
} }
@@ -59,6 +139,21 @@
</script> </script>
<template>
<!-- #ifdef APP-PLUS -->
<!-- 更新弹窗仅APP端 -->
<UpdateModal
:show="showUpdateModal"
:version="updateInfo.version"
:updateContent="updateInfo.updateContent"
:downloadUrl="updateInfo.downloadUrl"
:forceUpdate="updateInfo.forceUpdate"
@confirm="handleUpdateConfirm"
@cancel="handleUpdateCancel"
/>
<!-- #endif -->
</template>
<style lang="scss"> <style lang="scss">
/*每个页面公共css */ /*每个页面公共css */
@import 'uview-ui/index.scss'; @import 'uview-ui/index.scss';

View File

@@ -4,6 +4,12 @@
//export const BASE_URL = 'http://yishi.com' //export const BASE_URL = 'http://yishi.com'
export const BASE_URL = 'https://ckbapi.quwanzhi.com' export const BASE_URL = 'https://ckbapi.quwanzhi.com'
// APP配置
export const APP_CONFIG = {
version: '1.1.0', // 当前APP版本号与manifest.json保持一致
type: 'aiStore' // APP类型标识
}
// 获取请求头 // 获取请求头
const getHeaders = (options = {}) => { const getHeaders = (options = {}) => {
const token = uni.getStorageSync('token'); const token = uni.getStorageSync('token');

View File

@@ -0,0 +1,19 @@
import { request, APP_CONFIG } from '../config'
// APP相关API
export const appApi = {
// 检测APP更新
// @param {string} version - APP版本号可选默认使用配置中的版本号
// @returns {Promise} 返回更新信息
checkUpdate: (version) => {
return request({
url: '/v1/app/update',
method: 'GET',
data: {
type: APP_CONFIG.type, // 固定为 aiStore
version: version || APP_CONFIG.version // 使用传入的版本号或配置中的默认版本号
}
})
}
}

View File

@@ -116,6 +116,21 @@
</view> </view>
</view> </view>
</view> </view>
<!-- #ifdef APP-PLUS -->
<view class="module-item" @tap="handleCheckUpdate">
<view class="module-left">
<view class="module-icon green">
<text class="iconfont icon-shezhi" style="color: #33cc99; font-size: 24px;"></text>
</view>
<view class="module-info">
<text class="module-name">检查更新</text>
<text class="module-desc" v-if="!checkingUpdate && !hasNewVersion">当前版本 {{ currentVersion }}</text>
<text class="module-desc" v-if="checkingUpdate" style="color: #33cc99;">检查中...</text>
<text class="module-desc" v-if="!checkingUpdate && hasNewVersion" style="color: #ff6699;">发现新版本 {{ latestVersion }}</text>
</view>
</view>
</view>
<!-- #endif -->
<view class="module-item" @tap="showSettings" v-if='hide'> <view class="module-item" @tap="showSettings" v-if='hide'>
<view class="module-left"> <view class="module-left">
<view class="module-icon gray"> <view class="module-icon gray">
@@ -158,6 +173,16 @@
@close="closeLoginPage" @close="closeLoginPage"
@login-success="handleLoginSuccess" @login-success="handleLoginSuccess"
></login-register> ></login-register>
<!-- 更新弹窗 -->
<update-dialog
:show="showUpdateDialog"
:version="updateInfo.version"
:updateContent="updateInfo.updateContent"
:downloadUrl="updateInfo.downloadUrl"
:forceUpdate="updateInfo.forceUpdate"
@close="closeUpdateDialog"
></update-dialog>
</view> </view>
</template> </template>
@@ -168,8 +193,10 @@
import LoginRegister from './LoginRegister.vue'; import LoginRegister from './LoginRegister.vue';
import DataStatistics from './DataStatistics.vue'; import DataStatistics from './DataStatistics.vue';
import CustomerManagement from './CustomerManagement.vue'; import CustomerManagement from './CustomerManagement.vue';
import UpdateDialog from './UpdateDialog.vue';
import { hasValidToken, clearToken, redirectToLogin } from '../api/utils/auth'; import { hasValidToken, clearToken, redirectToLogin } from '../api/utils/auth';
import { request } from '../api/config'; import { request, APP_CONFIG } from '../api/config';
import { appApi } from '../api/modules/app';
export default { export default {
name: "SideMenu", name: "SideMenu",
@@ -179,7 +206,8 @@
SystemSettings, SystemSettings,
LoginRegister, LoginRegister,
DataStatistics, DataStatistics,
CustomerManagement CustomerManagement,
UpdateDialog
}, },
props: { props: {
show: { show: {
@@ -204,7 +232,20 @@
showCustomerManagementPage: false, showCustomerManagementPage: false,
showLoginPageFlag: false, showLoginPageFlag: false,
isLoggedIn: false, // 用户登录状态 isLoggedIn: false, // 用户登录状态
userInfo: null // 用户信息 userInfo: null, // 用户信息
// 版本信息
currentVersion: APP_CONFIG.version, // 当前版本
latestVersion: '', // 最新版本
hasNewVersion: false, // 是否有新版本
checkingUpdate: false, // 是否正在检查更新
// 更新弹窗
showUpdateDialog: false,
updateInfo: {
version: '',
updateContent: '',
downloadUrl: '',
forceUpdate: false
}
} }
}, },
watch: { watch: {
@@ -454,6 +495,83 @@
showSettings() { showSettings() {
// 显示系统设置页面 // 显示系统设置页面
this.showSystemSettingsPage = true; this.showSystemSettingsPage = true;
},
// 版本号比较函数
// 返回值: 1表示v1>v2, -1表示v1<v2, 0表示相等
compareVersion(v1, v2) {
const arr1 = v1.split('.').map(Number);
const arr2 = v2.split('.').map(Number);
const maxLen = Math.max(arr1.length, arr2.length);
for (let i = 0; i < maxLen; i++) {
const num1 = arr1[i] || 0;
const num2 = arr2[i] || 0;
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
return 0;
},
// 检查更新
async handleCheckUpdate() {
// #ifdef APP-PLUS
if (this.checkingUpdate) {
return; // 正在检查中,避免重复请求
}
this.checkingUpdate = true;
try {
console.log('开始检查更新,当前版本:', this.currentVersion);
const res = await appApi.checkUpdate();
console.log('更新检测结果:', res);
if (res.code === 200 && res.data) {
const data = res.data;
this.latestVersion = data.version || '';
// 比较版本号
const compareResult = this.compareVersion(this.latestVersion, this.currentVersion);
if (compareResult > 0) {
// 线上版本大于本地版本
this.hasNewVersion = true;
// 设置更新信息并显示自定义弹窗
this.updateInfo = {
version: data.version || '',
updateContent: data.updateContent || '',
downloadUrl: data.downloadUrl ? data.downloadUrl.trim() : '',
forceUpdate: data.forceUpdate || false
};
this.showUpdateDialog = true;
} else {
// 已是最新版本
this.hasNewVersion = false;
uni.showToast({
title: '已是最新版本',
icon: 'success'
});
}
}
} catch (error) {
console.error('检查更新失败:', error);
uni.showToast({
title: '检查更新失败',
icon: 'none'
});
} finally {
this.checkingUpdate = false;
}
// #endif
},
// 关闭更新弹窗
closeUpdateDialog() {
this.showUpdateDialog = false;
} }
} }
} }

View File

@@ -0,0 +1,556 @@
<template>
<view class="update-dialog-mask" v-if="show" @tap.stop="handleMaskClick">
<view class="update-dialog-container" @tap.stop>
<!-- 火箭图标 -->
<view class="rocket-container">
<view class="rocket-wrapper">
<!-- 火箭 SVG 图片 -->
<image class="rocket-svg" :src="rocketBase64" mode="aspectFit"></image>
<!-- 火焰效果 -->
<view class="flame-container">
<view class="flame flame-1"></view>
<view class="flame flame-2"></view>
<view class="flame flame-3"></view>
</view>
<!-- 星星装饰 -->
<view class="star star-1"></view>
<view class="star star-2"></view>
<view class="star star-3"></view>
<view class="star star-4"></view>
</view>
</view>
<!-- 内容区域 -->
<view class="dialog-content">
<!-- 强制更新提示 -->
<view class="force-notice" v-if="forceUpdate">
<text class="force-notice-icon"></text>
<text class="force-notice-text">本次为重要更新需要立即升级</text>
</view>
<!-- 更新内容 -->
<view class="update-content">
<text class="update-item" v-for="(item, index) in updateList" :key="index">{{ index + 1 }}.{{ item }}</text>
</view>
<!-- 下载进度 -->
<view class="progress-container" v-if="downloading">
<view class="progress-bar">
<view class="progress-fill" :style="{width: downloadProgress + '%'}"></view>
</view>
<text class="progress-text">{{ downloadProgress }}%</text>
</view>
<!-- 按钮 -->
<view class="button-container" v-if="!downloading">
<button class="update-button" @tap="handleUpdate">即刻升级</button>
</view>
<!-- 下载中按钮 -->
<view class="button-container" v-else>
<button class="update-button downloading">下载中...</button>
</view>
</view>
<!-- 关闭按钮 -->
<view class="close-button" @tap="handleClose" v-if="!forceUpdate && !downloading">
<text class="close-icon">×</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'UpdateDialog',
props: {
show: {
type: Boolean,
default: false
},
version: {
type: String,
default: ''
},
updateContent: {
type: String,
default: ''
},
downloadUrl: {
type: String,
default: ''
},
forceUpdate: {
type: Boolean,
default: false
}
},
data() {
return {
downloading: false,
downloadProgress: 0,
downloadTask: null,
// 火箭 SVG 的 base64 图片
rocketBase64: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzYxODA0ODYyMzIwIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjIwNDAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMzk1LjI2NCAzMDQuMTI4Yy03MC42NTYgOTIuMTYtMTQ1LjQwOCAxOTQuNTYtMTg5LjQ0IDMwNC4xMjgtNS4xMiAxMi4yODggNi4xNDQgMjMuNTUyIDE4LjQzMiAyMC40OEwzNTguNCA1OTAuODQ4TTYyOC43MzYgMzA0LjEyOGM3MC42NTYgOTIuMTYgMTQ1LjQwOCAxOTQuNTYgMTg5LjQ0IDMwNC4xMjggNS4xMiAxMi4yODgtNi4xNDQgMjMuNTUyLTE4LjQzMiAyMC40OEw2NjUuNiA1OTAuODQ4IiBmaWxsPSIjRjc5ODM5IiBwLWlkPSIyMDQxIj48L3BhdGg+PHBhdGggZD0iTTY3Ni44NjQgNzExLjY4SDM0NS4wODhDMzE4LjQ2NCA2MjQuNjQgMzEyLjMyIDUzMi40OCAzMzEuNzc2IDQ0My4zOTJjMjIuNTI4LTEwMS4zNzYgNzAuNjU2LTE5Ny42MzIgMTQwLjI4OC0yNzcuNTA0bDcuMTY4LTguMTkyYzE2LjM4NC0xOS40NTYgNDYuMDgtMTkuNDU2IDYyLjQ2NCAwbDcuMTY4IDguMTkyYzcwLjY1NiA3OS44NzIgMTE3Ljc2IDE3Ni4xMjggMTQwLjI4OCAyNzcuNTA0IDIwLjQ4IDg5LjA4OCAxNC4zMzYgMTgxLjI0OC0xMi4yODggMjY4LjI4OHoiIGZpbGw9IiMwMDRGRkYiIHAtaWQ9IjIwNDIiPjwvcGF0aD48cGF0aCBkPSJNNDY3Ljk2OCA2NzUuODRjLTUxLjIgMC05NS4yMzItMzcuODg4LTEwMi40LTg4LjA2NC04LjE5Mi02MC40MTYtNi4xNDQtMTIwLjgzMiA2LjE0NC0xODAuMjI0IDIxLjUwNC05NS4yMzIgNjQuNTEyLTE4NS4zNDQgMTI2Ljk3Ni0yNjIuMTQ0LTguMTkyIDIuMDQ4LTE1LjM2IDYuMTQ0LTIwLjQ4IDEyLjI4OGwtNy4xNjggOC4xOTJDNDAyLjQzMiAyNDUuNzYgMzU0LjMwNCAzNDAuOTkyIDMzMS43NzYgNDQzLjM5MiAzMTIuMzIgNTMyLjQ4IDMxOC40NjQgNjI0LjY0IDM0NS4wODggNzExLjY4aDMzMS43NzZjNC4wOTYtMTIuMjg4IDcuMTY4LTIzLjU1MiAxMC4yNC0zNS44NEg0NjcuOTY4eiIgZmlsbD0iIzFENkZGRiIgcC1pZD0iMjA0MyI+PC9wYXRoPjxwYXRoIGQ9Ik0zODEuOTUyIDcyMS45MmgyMzYuNTQ0Vjc3OC4yNEgzODEuOTUyeiIgZmlsbD0iIzAwNEZGRiIgcC1pZD0iMjA0NCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTQuNjk5Mjc2MjUgNDc0LjA2MzEyNjMxSDUwOC42MTAyMTEyM2wzLjA0NDUzMjUxIDIuODg3NTk3ODV6IiBmaWxsPSIjZmZmZmZmIiBwLWlkPSIyMDQ1Ij48L3BhdGg+PHBhdGggZD0iTTQzMC4wOCA0MjcuMDA4YTgwLjg5NiA3OS44NzIgMCAxIDAgMTYxLjc5MiAwIDgwLjg5NiA3OS44NzIgMCAxIDAgLTE2MS43OTIgMFoiIGZpbGw9IiNFOUYzRkIiIHAtaWQ9IjIwNDYiPjwvcGF0aD48L3N2Zz4='
}
},
computed: {
updateList() {
if (!this.updateContent) {
return ['修复已知问题', '优化用户体验', '提升系统稳定性'];
}
// 将更新内容按换行或分号分割
return this.updateContent.split(/[\n;]/).filter(item => item.trim());
}
},
methods: {
handleMaskClick() {
if (!this.forceUpdate && !this.downloading) {
this.handleClose();
}
},
handleClose() {
if (this.forceUpdate || this.downloading) {
return;
}
this.$emit('close');
},
handleUpdate() {
// #ifdef APP-PLUS
if (this.downloading) {
return;
}
if (!this.downloadUrl) {
uni.showToast({
title: '下载地址无效',
icon: 'none'
});
return;
}
this.downloading = true;
this.downloadProgress = 0;
// 创建下载任务
const downloadTask = uni.downloadFile({
url: this.downloadUrl.trim(),
success: (res) => {
if (res.statusCode === 200) {
console.log('下载成功,文件路径:', res.tempFilePath);
// 下载完成,安装应用
this.installApp(res.tempFilePath);
} else {
console.error('下载失败,状态码:', res.statusCode);
uni.showToast({
title: '下载失败',
icon: 'none'
});
this.downloading = false;
}
},
fail: (err) => {
console.error('下载失败:', err);
uni.showToast({
title: '下载失败,请稍后重试',
icon: 'none'
});
this.downloading = false;
}
});
// 监听下载进度
downloadTask.onProgressUpdate((res) => {
this.downloadProgress = res.progress;
console.log('下载进度:', res.progress + '%');
});
this.downloadTask = downloadTask;
// #endif
},
installApp(filePath) {
// #ifdef APP-PLUS
console.log('开始安装应用:', filePath);
plus.runtime.install(
filePath,
{
force: false
},
() => {
console.log('安装成功');
uni.showToast({
title: '安装成功,请重启应用',
icon: 'success',
duration: 2000
});
// 安装成功后关闭弹窗
setTimeout(() => {
this.downloading = false;
this.downloadProgress = 0;
this.$emit('close');
// 如果是强制更新,重启应用
if (this.forceUpdate) {
plus.runtime.restart();
}
}, 2000);
},
(error) => {
console.error('安装失败:', error);
uni.showToast({
title: '安装失败',
icon: 'none'
});
this.downloading = false;
}
);
// #endif
}
},
beforeDestroy() {
// 组件销毁时,取消下载任务
if (this.downloadTask) {
this.downloadTask.abort();
}
}
}
</script>
<style lang="scss" scoped>
.update-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999;
}
.update-dialog-container {
width: 580rpx;
background: linear-gradient(180deg, #4A9FF5 0%, #2E7FD9 100%);
border-radius: 32rpx;
position: relative;
overflow: visible;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
}
.rocket-container {
position: absolute;
top: -120rpx;
left: 50%;
transform: translateX(-50%);
width: 200rpx;
height: 200rpx;
display: flex;
align-items: center;
justify-content: center;
}
.rocket-wrapper {
position: relative;
width: 100%;
height: 100%;
animation: rocketFloat 2s ease-in-out infinite;
}
/* 火箭 SVG 图片 */
.rocket-svg {
position: absolute;
top: 10rpx;
left: 50%;
transform: translateX(-50%);
width: 160rpx;
height: 160rpx;
z-index: 10;
}
/* 火焰容器 */
.flame-container {
position: absolute;
bottom: 10rpx;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
z-index: 5;
}
/* 火焰效果 */
.flame {
position: absolute;
border-radius: 50%;
animation: flameFlicker 0.3s ease-in-out infinite alternate;
}
.flame-1 {
width: 40rpx;
height: 30rpx;
background: radial-gradient(ellipse at center, #FCD34D 0%, #FBBF24 50%, transparent 80%);
top: 0;
animation-delay: 0s;
}
.flame-2 {
width: 30rpx;
height: 40rpx;
background: radial-gradient(ellipse at center, #FBBF24 0%, #F59E0B 50%, transparent 80%);
top: 15rpx;
animation-delay: 0.1s;
}
.flame-3 {
width: 20rpx;
height: 35rpx;
background: radial-gradient(ellipse at center, #F59E0B 0%, #EF4444 50%, transparent 80%);
top: 30rpx;
animation-delay: 0.2s;
}
/* 星星装饰 */
.star {
position: absolute;
width: 8rpx;
height: 8rpx;
background: #FCD34D;
border-radius: 50%;
box-shadow: 0 0 10rpx #FCD34D;
}
.star-1 {
top: 30rpx;
left: 20rpx;
animation: starTwinkle 2s ease-in-out infinite;
animation-delay: 0s;
}
.star-2 {
top: 50rpx;
right: 15rpx;
animation: starTwinkle 2s ease-in-out infinite;
animation-delay: 0.5s;
}
.star-3 {
top: 80rpx;
left: 10rpx;
animation: starTwinkle 1.5s ease-in-out infinite;
animation-delay: 1s;
}
.star-4 {
top: 100rpx;
right: 20rpx;
animation: starTwinkle 1.5s ease-in-out infinite;
animation-delay: 1.5s;
}
/* 动画定义 */
@keyframes rocketFloat {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-15rpx);
}
}
@keyframes flameFlicker {
0% {
opacity: 0.8;
transform: scaleY(1);
}
100% {
opacity: 1;
transform: scaleY(1.2);
}
}
@keyframes starTwinkle {
0%, 100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.5);
}
}
.dialog-content {
padding: 50rpx 40rpx 40rpx;
background: #FFFFFF;
border-radius: 32rpx;
margin-top: 80rpx;
}
.dialog-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
text-align: center;
margin-bottom: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.force-tag {
display: inline-block;
margin-top: 10rpx;
padding: 4rpx 16rpx;
background: linear-gradient(135deg, #FF6B6B 0%, #FF4757 100%);
color: #FFFFFF;
font-size: 20rpx;
border-radius: 20rpx;
font-weight: normal;
animation: tagPulse 1.5s ease-in-out infinite;
}
@keyframes tagPulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.7);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 0 8rpx rgba(255, 71, 87, 0);
}
}
.force-notice {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx;
margin-bottom: 24rpx;
background: linear-gradient(135deg, #FFF5F5 0%, #FFE5E5 100%);
border-radius: 16rpx;
border: 2rpx solid #FFB8B8;
}
.force-notice-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.force-notice-text {
font-size: 26rpx;
color: #FF4757;
font-weight: 500;
}
.update-content {
background: #F8F9FA;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 40rpx;
max-height: 300rpx;
overflow-y: auto;
}
.update-item {
display: block;
font-size: 28rpx;
color: #666666;
line-height: 44rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.progress-container {
margin-bottom: 40rpx;
}
.progress-bar {
width: 100%;
height: 12rpx;
background: #E5E7EB;
border-radius: 6rpx;
overflow: hidden;
margin-bottom: 16rpx;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4A9FF5 0%, #2E7FD9 100%);
border-radius: 6rpx;
transition: width 0.3s ease;
}
.progress-text {
display: block;
text-align: center;
font-size: 24rpx;
color: #4A9FF5;
font-weight: bold;
}
.button-container {
width: 100%;
}
.update-button {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #4A9FF5 0%, #2E7FD9 100%);
border-radius: 44rpx;
border: none;
color: #FFFFFF;
font-size: 32rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(46, 127, 217, 0.4);
&.downloading {
background: #CCCCCC;
box-shadow: none;
}
&::after {
border: none;
}
}
.close-button {
position: absolute;
bottom: -120rpx;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 80rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid rgba(255, 255, 255, 0.6);
}
.close-icon {
font-size: 60rpx;
color: #FFFFFF;
line-height: 1;
font-weight: 300;
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<view class="update-modal" v-if="show" @tap.stop="handleMaskClick">
<view class="modal-content" @tap.stop>
<!-- 版本号标题 -->
<view class="version-title">发现新版本 {{ version }}</view>
<!-- 更新内容 -->
<view class="update-content">
<text class="content-text">{{ updateContent }}</text>
</view>
<!-- 按钮区域 -->
<view class="button-area">
<view class="cancel-btn" v-if="!forceUpdate" @tap="handleCancel">稍后再说</view>
<view class="confirm-btn" @tap="handleConfirm">立即更新</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'UpdateModal',
props: {
show: {
type: Boolean,
default: false
},
version: {
type: String,
default: ''
},
updateContent: {
type: String,
default: ''
},
downloadUrl: {
type: String,
default: ''
},
forceUpdate: {
type: Boolean,
default: false
}
},
methods: {
handleMaskClick() {
// 强制更新时,点击遮罩不关闭
if (!this.forceUpdate) {
this.handleCancel();
}
},
handleCancel() {
this.$emit('cancel');
},
handleConfirm() {
this.$emit('confirm', this.downloadUrl);
}
}
}
</script>
<style lang="scss" scoped>
.update-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
.modal-content {
width: 600rpx;
background-color: #ffffff;
border-radius: 24rpx;
overflow: hidden;
.version-title {
text-align: center;
font-size: 36rpx;
font-weight: 600;
color: #333333;
padding: 60rpx 40rpx 40rpx;
}
.update-content {
max-height: 600rpx;
padding: 0 40rpx 40rpx;
overflow-y: auto;
.content-text {
font-size: 28rpx;
line-height: 44rpx;
color: #666666;
white-space: pre-wrap;
word-break: break-all;
}
}
.button-area {
display: flex;
border-top: 1px solid #eeeeee;
.cancel-btn,
.confirm-btn {
flex: 1;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 500;
}
.cancel-btn {
color: #999999;
border-right: 1px solid #eeeeee;
}
.confirm-btn {
color: #007aff;
}
}
}
}
</style>

View File

@@ -2,8 +2,8 @@
"name" : "AI数智员工", "name" : "AI数智员工",
"appid" : "__UNI__9421F6C", "appid" : "__UNI__9421F6C",
"description" : "", "description" : "",
"versionName" : "1.0.1", "versionName" : "1.1.0",
"versionCode" : "100", "versionCode" : 100,
"transformPx" : false, "transformPx" : false,
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus" : {
@@ -107,5 +107,17 @@
}, },
"vueVersion" : "2", "vueVersion" : "2",
"locale" : "zh-Hans", "locale" : "zh-Hans",
"fallbackLocale" : "zh-Hans" "fallbackLocale" : "zh-Hans",
/* H5 */
"h5" : {
"router" : {
"mode" : "hash",
"base" : "./"
},
"title" : "AI数智员工",
"devServer" : {
"port" : 8080,
"disableHostCheck" : true
}
}
} }

View File

@@ -4,15 +4,15 @@
}, },
"pages": [ "pages": [
{ {
"path": "pages/chat/index", "path": "pages/login/index",
"style": { "style": {
"navigationBarTitleText": "AI数智员工",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{ {
"path": "pages/login/index", "path": "pages/chat/index",
"style": { "style": {
"navigationBarTitleText": "AI数智员工",
"navigationStyle": "custom" "navigationStyle": "custom"
} }
} }

View File

@@ -1,80 +1,214 @@
<template> <template>
<view class="login-page"> <view class="login-page">
<!-- 页面顶部导航 -->
<view class="page-header"> <!-- #ifdef APP-PLUS -->
<view class="back-btn"></view> <!-- APP端返回按钮仅显示登录表单时 -->
<view class="page-title">登录/注册</view> <view class="back-button" v-if="showLoginForm" @tap="handleBack">
<u-icon name="arrow-left" size="24" color="#333"></u-icon>
</view> </view>
<!-- 内容区域 --> <!-- APP端主界面 -->
<view class="content-area"> <view class="main-screen" v-if="!showLoginForm">
<!-- 登录标题 --> <!-- Logo 区域 -->
<view class="login-title">账号密码登录</view> <view class="logo-section">
<image class="logo-image" src="/static/logo.png" mode="aspectFit"></image>
<!-- 账号输入 --> <text class="app-title">AI数智员工</text>
<view class="form-item">
<view class="input-item">
<u-input
v-model="account"
placeholder="请输入账号"
class="input-field"
border="0"
prefixIcon="account"
/>
</view>
</view> </view>
<!-- 密码输入 --> <!-- 主按钮区域 -->
<view class="form-item"> <view class="main-buttons">
<view class="input-item"> <!-- 免密登录按钮 -->
<u-input <u-button
:type="passwordVisible ? 'text' : 'password'" text="免密登录"
v-model="password" type="primary"
placeholder="请输入密码" shape="circle"
class="input-field" @click="showQuickLogin"
border="0" :custom-style="{width: '100%', marginBottom: '30rpx', backgroundColor: '#4A90E2'}"
prefixIcon="lock" ></u-button>
/>
<view class="password-icon" @tap="passwordVisible = !passwordVisible"> <!-- 账号登录按钮 -->
<u-icon :name="passwordVisible ? 'eye' : 'eye-off'" size="20" color="#999"></u-icon> <u-button
</view> text="账号登录"
</view> shape="circle"
:hairline="false"
@click="showAccountLogin"
:custom-style="{width: '100%', backgroundColor: '#f5f5f5', color: '#666', border: 'none'}"
></u-button>
</view> </view>
<!-- 用户协议 --> <!-- 用户协议 -->
<view class="agreement-container"> <view class="agreement-container">
<checkbox-group @change="checkboxChange"> <checkbox-group @change="checkboxChange">
<checkbox :value="agreement" :checked="agreement" class="agreement-checkbox" color="#4080ff" /> <checkbox :value="agreement" :checked="agreement" class="agreement-checkbox" color="#4A90E2" />
</checkbox-group> </checkbox-group>
<text class="agreement-text">阅读并同意</text> <text class="agreement-text">阅读并同意</text>
<text class="agreement-link" @tap="openAgreement('user')">用户协议</text> <text class="agreement-link" @tap="openAgreement('user')">用户协议</text>
<text class="agreement-text"></text> <text class="agreement-text"></text>
<text class="agreement-link" @tap="openAgreement('privacy')">隐私政策</text> <text class="agreement-link" @tap="openAgreement('privacy')">隐私权限</text>
</view>
<!-- 登录按钮 -->
<view
class="login-btn"
:class="{ active: canLogin }"
@tap="handleLogin"
>
登录
</view>
<!-- 免密登录按钮 -->
<view
class="no-password-login-btn"
@tap="handleNoPasswordLogin"
>
免密登录
</view>
<!-- 联系我们 -->
<view class="contact-us" @tap="contactUs">
联系我们
</view> </view>
</view> </view>
<!-- APP端登录表单 -->
<view v-if="showLoginForm" class="login-form">
<!-- Logo 区域 -->
<view class="logo-section-form">
<image class="logo-image-form" src="/static/logo.png" mode="aspectFit"></image>
</view>
<text class="form-title">{{ formTitle }}</text>
<!-- 账号登录表单 -->
<view v-if="loginMode === 'account'" class="form-content">
<!-- 账号输入 -->
<view class="input-group">
<text class="input-label">账号</text>
<u-input
v-model="account"
:clearable="true"
placeholder="请输入账号"
:border="true"
></u-input>
</view>
<!-- 密码输入 -->
<view class="input-group">
<text class="input-label">密码</text>
<u-input
v-model="password"
type="password"
:password="!passwordVisible"
placeholder="请输入密码"
:border="true"
>
<template slot="suffix">
<u-icon
:name="passwordVisible ? 'eye-fill' : 'eye-off'"
size="22"
color="#c0c4cc"
@click="passwordVisible = !passwordVisible"
></u-icon>
</template>
</u-input>
</view>
<!-- 登录按钮 -->
<u-button
text="登录"
type="primary"
shape="circle"
:disabled="!canLogin"
@click="handleLogin"
:custom-style="{marginTop: '60rpx', width: '100%', backgroundColor: '#4A90E2'}"
></u-button>
<!-- 用户协议 -->
<view class="agreement-container form-agreement">
<checkbox-group @change="checkboxChange">
<checkbox :value="agreement" :checked="agreement" class="agreement-checkbox" color="#4A90E2" />
</checkbox-group>
<text class="agreement-text">阅读并同意</text>
<text class="agreement-link" @tap="openAgreement('user')">用户协议</text>
<text class="agreement-text"></text>
<text class="agreement-link" @tap="openAgreement('privacy')">隐私权限</text>
</view>
</view>
<!-- 免密登录表单 -->
<view v-else-if="loginMode === 'quick'" class="form-content">
<!-- 设备ID显示仅用于调试 -->
<view class="input-group">
<text class="input-label">设备ID</text>
<u-input
v-model="deviceId"
:disabled="true"
placeholder="自动获取设备ID"
:border="true"
></u-input>
</view>
<!-- 登录按钮 -->
<u-button
text="登录"
type="primary"
shape="circle"
:disabled="!deviceId"
@click="handleNoPasswordLogin"
:custom-style="{marginTop: '60rpx', width: '100%', backgroundColor: '#4A90E2'}"
></u-button>
</view>
</view>
<!-- #endif -->
<!-- #ifndef APP-PLUS -->
<!-- 非APP端直接显示账号登录表单 -->
<view class="login-form h5-login">
<!-- Logo 区域 -->
<view class="logo-section-form">
<image class="logo-image-form" src="/static/logo.png" mode="aspectFit"></image>
</view>
<text class="form-title">{{ formTitle }}</text>
<view class="form-content">
<!-- 账号输入 -->
<view class="input-group">
<text class="input-label">账号</text>
<u-input
v-model="account"
:clearable="true"
placeholder="请输入账号"
:border="true"
></u-input>
</view>
<!-- 密码输入 -->
<view class="input-group">
<text class="input-label">密码</text>
<u-input
v-model="password"
type="password"
:password="!passwordVisible"
placeholder="请输入密码"
:border="true"
>
<template slot="suffix">
<u-icon
:name="passwordVisible ? 'eye-fill' : 'eye-off'"
size="22"
color="#c0c4cc"
@click="passwordVisible = !passwordVisible"
></u-icon>
</template>
</u-input>
</view>
<!-- 登录按钮 -->
<u-button
text="登录"
type="primary"
shape="circle"
:disabled="!canLogin"
@click="handleLogin"
:custom-style="{marginTop: '60rpx', width: '100%', backgroundColor: '#4A90E2'}"
></u-button>
<!-- 用户协议 -->
<view class="agreement-container form-agreement">
<checkbox-group @change="checkboxChange">
<checkbox :value="agreement" :checked="agreement" class="agreement-checkbox" color="#4A90E2" />
</checkbox-group>
<text class="agreement-text">阅读并同意</text>
<text class="agreement-link" @tap="openAgreement('user')">用户协议</text>
<text class="agreement-text"></text>
<text class="agreement-link" @tap="openAgreement('privacy')">隐私权限</text>
</view>
</view>
</view>
<!-- #endif -->
</view> </view>
</template> </template>
<script> <script>
@@ -85,11 +219,19 @@
export default { export default {
data() { data() {
return { return {
// #ifdef APP-PLUS
showLoginForm: false, // APP端是否显示登录表单
loginMode: '', // APP端'account' 或 'quick'
// #endif
// #ifndef APP-PLUS
showLoginForm: true, // 非APP端始终显示登录表单
loginMode: 'account', // 非APP端始终为账号登录
// #endif
account: '', // 账号 account: '', // 账号
password: '', // 密码 password: '', // 密码
passwordVisible: false, // 密码是否可见 passwordVisible: false, // 密码是否可见
agreement: true, // 是否同意协议 agreement: true, // 是否同意协议
deviceId: '' // 设备ID deviceId: '' // 设备ID
} }
}, },
// 页面加载时检查token // 页面加载时检查token
@@ -102,15 +244,65 @@
this.checkTokenStatus(); this.checkTokenStatus();
}, },
computed: { computed: {
formTitle() {
// #ifdef APP-PLUS
return this.loginMode === 'account' ? '账号登录' : '免密登录';
// #endif
// #ifndef APP-PLUS
return '账号登录';
// #endif
},
// 验证是否可以登录 // 验证是否可以登录
canLogin() { canLogin() {
return this.account && // #ifdef APP-PLUS
this.password && if (this.loginMode === 'account') {
this.password.length >= 6 && return this.account && this.password && this.password.length >= 6 && this.agreement;
this.agreement; } else if (this.loginMode === 'quick') {
return this.deviceId && this.agreement;
}
return false;
// #endif
// #ifndef APP-PLUS
return this.account && this.password && this.password.length >= 6 && this.agreement;
// #endif
} }
}, },
methods: { methods: {
// 返回主界面
handleBack() {
this.showLoginForm = false;
this.loginMode = '';
this.account = '';
this.password = '';
},
// 免密登录 - 直接登录,不跳转页面
async showQuickLogin() {
if (!this.agreement) {
uni.showToast({
title: '请先同意用户协议和隐私政策',
icon: 'none'
});
return;
}
// 直接调用免密登录
await this.handleNoPasswordLogin();
},
// 显示账号登录表单
showAccountLogin() {
if (!this.agreement) {
uni.showToast({
title: '请先同意用户协议和隐私政策',
icon: 'none'
});
return;
}
this.loginMode = 'account';
this.showLoginForm = true;
},
// 获取设备ID仅APP端 // 获取设备ID仅APP端
getDeviceId() { getDeviceId() {
// #ifdef APP-PLUS // #ifdef APP-PLUS
@@ -135,10 +327,10 @@
} }
// #endif // #endif
// #ifdef H5 // #ifndef APP-PLUS
// H5端不传设备ID // 非APP端不传设备ID
this.deviceId = ''; this.deviceId = '';
console.log('H5端不传设备ID'); console.log('非APP端不传设备ID');
// #endif // #endif
}, },
@@ -150,17 +342,11 @@
} }
}, },
// 返回上一页
goBack() {
uni.navigateBack();
},
// 用户协议复选框变化 // 用户协议复选框变化
checkboxChange(){ checkboxChange(){
this.agreement = !this.agreement this.agreement = !this.agreement
}, },
// 处理登录 // 处理登录
async handleLogin() { async handleLogin() {
// 检查是否同意协议 // 检查是否同意协议
@@ -211,8 +397,8 @@
// #ifdef APP-PLUS // #ifdef APP-PLUS
console.log('APP端登录 - 设备ID:', this.deviceId); console.log('APP端登录 - 设备ID:', this.deviceId);
// #endif // #endif
// #ifdef H5 // #ifndef APP-PLUS
console.log('H5端登录 - 不传设备ID'); console.log('非APP端登录 - 不传设备ID');
// #endif // #endif
if (response.code === 200) { // 成功code是200 if (response.code === 200) { // 成功code是200
@@ -284,7 +470,7 @@
uni.setStorageSync('token_expired', token_expired); uni.setStorageSync('token_expired', token_expired);
uni.showToast({ uni.showToast({
title: '免密登录成功', title: '登录成功',
icon: 'success' icon: 'success'
}); });
@@ -313,19 +499,12 @@
// 打开协议 // 打开协议
openAgreement(type) { openAgreement(type) {
// TODO: 跳转到对应的协议页面
const title = type === 'user' ? '用户协议' : '隐私权限';
uni.showToast({ uni.showToast({
title: `打开${type === 'user' ? '用户协议' : '隐私政策'}`, title: `打开${title}`,
icon: 'none' icon: 'none'
}); });
},
// 联系我们
contactUs() {
uni.showToast({
title: '联系方式: zhiqun@qq.com',
icon: 'none',
duration: 3000
});
} }
} }
} }
@@ -334,132 +513,153 @@
<style lang="scss"> <style lang="scss">
.login-page { .login-page {
min-height: 100vh; min-height: 100vh;
background-color: #fff; background-color: #ffffff;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-top: 40px; /* 为状态栏预留空间 */
}
.page-header {
display: flex;
align-items: center;
position: relative; position: relative;
padding: 10px 0;
margin-bottom: 10px;
} }
.back-btn { /* 返回按钮 */
.back-button {
position: absolute; position: absolute;
left: 15px; top: 40rpx;
width: 30px; left: 30rpx;
height: 30px; z-index: 100;
display: flex; padding: 10rpx;
align-items: center;
z-index: 2;
} }
.page-title { /* 主界面 */
text-align: center; .main-screen {
font-size: 18px;
font-weight: bold;
width: 100%; width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
} }
.content-area { /* Logo 区域 - 主界面 */
flex: 1; .logo-section {
padding: 0 30px; display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 120rpx;
} }
.login-title { .logo-image {
font-size: 24px; width: 200rpx;
height: 200rpx;
border-radius: 40rpx;
margin-bottom: 40rpx;
}
.app-title {
font-size: 40rpx;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
margin: 30px 0 40px;
text-align: center;
} }
.input-item { /* Logo 区域 - 登录表单 */
.logo-section-form {
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
border-bottom: 1px solid #eee; margin-bottom: 60rpx;
padding: 12px 0;
min-height: 50px;
position: relative;
} }
.input-field { .logo-image-form {
flex: 1; width: 200rpx;
height: 24px; height: 200rpx;
font-size: 15px; border-radius: 40rpx;
} }
.password-icon { /* 主按钮区域 */
position: absolute; .main-buttons {
right: 0; padding: 0 60rpx;
padding: 0 5px;
height: 100%;
display: flex; display: flex;
align-items: center; flex-direction: column;
margin-bottom: 40rpx;
} }
/* 登录表单 */
.login-form {
padding: 60rpx;
width: 100%;
min-height: 100vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
}
/* 表单内容区域 */
.form-content {
display: flex;
flex-direction: column;
}
/* H5端登录表单 */
.h5-login {
display: flex;
flex-direction: column;
justify-content: center;
padding: 60rpx;
}
.form-title {
font-size: 44rpx;
font-weight: bold;
color: #333;
margin-bottom: 60rpx;
display: block;
}
.input-group {
margin-bottom: 40rpx;
}
.input-label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
}
/* 用户协议 */
.agreement-container { .agreement-container {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 15px 0; justify-content: center;
padding: 40rpx 0 60rpx;
}
/* 表单内的用户协议 */
.form-agreement {
justify-content: flex-start;
padding: 30rpx 0 0;
margin-top: 40rpx;
}
.h5-agreement {
justify-content: flex-start;
padding: 30rpx 0;
margin-top: 0;
} }
.agreement-checkbox { .agreement-checkbox {
transform: scale(0.8); transform: scale(0.8);
margin-right: 5px; margin-right: 5rpx;
} }
.agreement-text { .agreement-text {
font-size: 13px; font-size: 24rpx;
color: #666; color: #999;
margin-left: 10rpx;
} }
.agreement-link { .agreement-link {
font-size: 13px; font-size: 24rpx;
color: #4080ff; color: #4A90E2;
margin: 0 4rpx;
} }
</style>
.login-btn {
height: 44px;
line-height: 44px;
text-align: center;
background-color: #dddddd;
color: #ffffff;
border-radius: 22px;
margin: 20px 0;
font-size: 16px;
transition: background-color 0.3s;
}
.login-btn.active {
background-color: #4080ff;
}
.no-password-login-btn {
height: 44px;
line-height: 44px;
text-align: center;
background-color: #fff;
color: #4080ff;
border: 1px solid #4080ff;
border-radius: 22px;
margin: 10px 0 20px;
font-size: 16px;
transition: all 0.3s;
}
.no-password-login-btn:active {
background-color: #f0f5ff;
}
.contact-us {
text-align: center;
color: #999;
font-size: 14px;
margin-top: 40px;
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 526 KiB