This commit is contained in:
sexygoat
2026-01-22 17:25:30 +08:00
commit 6bf56a4b4c
35 changed files with 6297 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"Bash(git add:*)"
],
"deny": [],
"ask": []
}
}

94
.gitignore vendored Normal file
View File

@@ -0,0 +1,94 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage/
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# uni-app
unpackage/
dist/
# VS Code
.vscode/
# IDE
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db

594
App.vue Normal file
View File

@@ -0,0 +1,594 @@
<script>
import {
userStore
} from '@/store/index.js';
// 判断是否是微信环境
function isWeChat() {
var ua = window.navigator.userAgent.toLowerCase();
if (ua.match(/micromessenger/i) == 'micromessenger') {
return true;
} else {
return false;
}
}
export default {
onLaunch: function() {
console.log('App Launch')
// 初始化用户登录状态
userStore.actions.initLoginState();
// 延迟检查登录状态,确保页面加载完成
// setTimeout(() => {
// this.checkLoginStatus();
// }, 100);
},
onShow: function() {
console.log('App Show')
// 在应用显示时检查登录状态
// this.checkLoginStatus();
// 判断是否是微信环境 当前不在error页面
// if (!isWeChat() && window.location.href.indexOf('error') === -1) {
// // 跳转到自定义的错误页面
// setTimeout(() => {
// window.location.href = 'https://m1.whjhft.com/singer-card/pages/error/error';
// }, 500);
// return;
// }
},
onHide: function() {
console.log('App Hide')
},
onUnload() {
// uni-app 页面卸载
this.handleExit();
},
methods: {
// 触发退出逻辑
async handleExit() {
await userStore.actions.logout()
},
// 检查登录状态和路由拦截
checkLoginStatus() {
try {
// 获取当前页面路径
const pages = getCurrentPages();
if (!pages || pages.length === 0) {
console.log('页面栈为空,跳过路由检查');
return;
}
const currentPage = pages[pages.length - 1];
const currentRoute = '/' + currentPage.route;
console.log('当前路由:', currentRoute);
// 获取登录状态通过device_id判断
const deviceId = uni.getStorageSync('device_id');
const isLoggedInStorage = uni.getStorageSync('isLoggedIn');
const isLoggedIn = !!deviceId && !!isLoggedInStorage;
console.log('登录状态:', isLoggedIn, '设备ID:', deviceId);
// 如果已登录但在登录页面,跳转到首页
if (isLoggedIn && currentRoute === '/pages/login/login') {
uni.showToast({
title: "已登录但在登录页面,跳转到首页",
icon: "none"
})
uni.setTimeout(() => {
uni.redirectTo({
url: '/pages/index/index',
fail: (err) => {
console.error('跳转到首页失败:', err);
}
})
}, 200)
return;
}
// 如果未登录且不在登录页面,跳转到登录页
if (!isLoggedIn && currentRoute !== '/pages/login/login') {
console.log('未登录,跳转到登录页');
uni.showToast({
title: "未登录,跳转到登录页",
icon: "none"
})
uni.setTimeout(() => {
uni.redirectTo({
url: '/pages/login/login',
fail: (err) => {
console.error('跳转到登录页失败:', err);
}
})
}, 200)
return;
}
console.log('路由检查通过,无需跳转');
} catch (error) {
console.error('路由检查出错:', error);
}
}
}
}
</script>
<style lang="scss">
/* 注意要写在第一行同时给style标签加入lang="scss"属性 */
@import "uview-plus/index.scss";
/* ================================
Apple Design System - Variables
================================ */
:root {
/* Apple Color Palette */
--apple-blue: #ff6800;
--apple-green: #34C759;
--apple-orange: #FF9500;
--apple-red: #FF3B30;
--apple-purple: #AF52DE;
--apple-pink: #FF2D92;
--apple-indigo: #5856D6;
--apple-teal: #5AC8FA;
/* Neutral Colors */
--system-gray: #8E8E93;
--system-gray-2: #AEAEB2;
--system-gray-3: #C7C7CC;
--system-gray-4: #D1D1D6;
--system-gray-5: #E5E5EA;
--system-gray-6: #F2F2F7;
/* Background Colors */
--background-primary: #FFFFFF;
--background-secondary: #F2F2F7;
--background-tertiary: #FFFFFF;
--background-grouped: #F2F2F7;
/* Text Colors */
--text-primary: #000000;
--text-secondary: #3C3C43;
--text-tertiary: #3C3C4399;
--text-quaternary: #3C3C4366;
/* Shadow System */
--shadow-small: 0 2rpx 4rpx rgba(0, 0, 0, 0.06);
--shadow-medium: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
--shadow-large: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
--shadow-extra-large: 0 16rpx 48rpx rgba(0, 0, 0, 0.16);
/* Border Radius */
--radius-small: 8rpx;
--radius-medium: 16rpx;
--radius-large: 24rpx;
--radius-extra-large: 32rpx;
/* Spacing System (8pt grid) */
--space-xs: 8rpx;
--space-sm: 16rpx;
--space-md: 24rpx;
--space-lg: 32rpx;
--space-xl: 40rpx;
--space-2xl: 48rpx;
/* Animation */
--transition-fast: 0.2s ease-out;
--transition-normal: 0.3s ease-out;
--transition-slow: 0.5s ease-out;
}
/* ================================
Global Reset & Base Styles
================================ */
* {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "MiSans", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
ul,
li {
list-style: none;
}
page {
color: var(--text-primary);
background-color: #f5f5f5;
}
/* ================================
Layout Components
================================ */
.container {
display: flex;
flex-direction: column;
gap: 20rpx;
padding: 30rpx 20rpx;
max-width: 750rpx;
margin: 0 auto;
}
.card {
background: var(--background-primary);
border-radius: 16rpx;
padding: 24rpx;
border: 1rpx solid var(--system-gray-5);
transition: all 0.2s ease;
}
.card-elevated {
box-shadow: var(--shadow-large);
transform: translateY(-4rpx);
}
.card-interactive {
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
/* ================================
Typography System
================================ */
.title {
font-weight: 700;
font-size: 34rpx;
line-height: 1.2;
color: var(--text-primary);
letter-spacing: -0.02em;
}
.subtitle {
font-weight: 600;
font-size: 28rpx;
line-height: 1.3;
color: var(--text-secondary);
}
.body {
font-weight: 400;
font-size: 28rpx;
line-height: 1.4;
color: var(--text-secondary);
}
.caption {
font-weight: 500;
font-size: 24rpx;
line-height: 1.3;
color: var(--text-tertiary);
}
/* ================================
Spacing Utilities
================================ */
.mt-xs {
margin-top: var(--space-xs);
}
.mt-sm {
margin-top: var(--space-sm);
}
.mt-md {
margin-top: var(--space-md);
}
.mt-lg {
margin-top: var(--space-lg);
}
.mt-xl {
margin-top: var(--space-xl);
}
.mt-30 {
margin-top: 30rpx;
}
/* 保持兼容性 */
.mt-20 {
margin-top: 20rpx;
}
/* 保持兼容性 */
.mb-xs {
margin-bottom: var(--space-xs);
}
.mb-sm {
margin-bottom: var(--space-sm);
}
.mb-md {
margin-bottom: var(--space-md);
}
.mb-lg {
margin-bottom: var(--space-lg);
}
.mb-xl {
margin-bottom: var(--space-xl);
}
.p-xs {
padding: var(--space-xs);
}
.p-sm {
padding: var(--space-sm);
}
.p-md {
padding: var(--space-md);
}
.p-lg {
padding: var(--space-lg);
}
.p-xl {
padding: var(--space-xl);
}
/* ================================
Flexbox Layout Utilities
================================ */
.flex-row {
display: flex;
align-items: center;
}
.flex-row-g8 {
display: flex;
align-items: center;
gap: var(--space-xs);
}
.flex-row-g16 {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.flex-row-g20 {
display: flex;
align-items: center;
gap: 20rpx;
/* 保持兼容性 */
}
.flex-row-g24 {
display: flex;
align-items: center;
gap: var(--space-md);
}
.flex-row-sb {
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-row-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-col {
display: flex;
flex-direction: column;
}
.flex-col-g8 {
display: flex;
flex-direction: column;
gap: var(--space-xs);
}
.flex-col-g16 {
display: flex;
flex-direction: column;
gap: var(--space-sm);
}
.flex-col-g20 {
display: flex;
flex-direction: column;
gap: 20rpx;
/* 保持兼容性 */
}
.flex-col-g24 {
display: flex;
flex-direction: column;
gap: var(--space-md);
}
.flex-col-center-g20 {
display: flex;
flex-direction: column;
gap: 20rpx;
/* 保持兼容性 */
align-items: center;
justify-content: center;
}
.flex-col-center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* ================================
Sizing Utilities
================================ */
.w-100 {
width: 100%;
}
.h-100 {
height: 100%;
}
/* ================================
Interactive Elements
================================ */
.interactive {
transition: all var(--transition-normal);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.button-apple {
background: var(--apple-blue);
color: white;
border: none;
border-radius: var(--radius-medium);
padding: var(--space-sm) var(--space-md);
font-weight: 600;
font-size: 28rpx;
transition: all var(--transition-normal);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
/* ================================
Tag Styles
================================ */
.tag-apple {
display: inline-flex;
align-items: center;
padding: var(--space-xs) var(--space-sm);
background: var(--system-gray-6);
color: var(--text-secondary);
border-radius: var(--radius-small);
font-size: 22rpx;
font-weight: 600;
letter-spacing: 0.02em;
&.tag-success {
background: rgba(52, 199, 89, 0.1);
color: var(--apple-green);
}
&.tag-primary {
background: rgba(0, 122, 255, 0.1);
color: var(--apple-blue);
}
&.tag-warning {
background: rgba(255, 149, 0, 0.1);
color: var(--apple-orange);
}
}
/* ================================
Progress Bar
================================ */
.progress-apple {
width: 100%;
height: 8rpx;
background: var(--system-gray-5);
border-radius: var(--radius-small);
overflow: hidden;
position: relative;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--apple-blue), var(--apple-teal));
border-radius: var(--radius-small);
transition: width var(--transition-normal);
}
}
/* ================================
Simple & Clean Button Styles
================================ */
.btn-apple {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 12rpx;
font-weight: 500;
font-size: 26rpx;
line-height: 1;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
-webkit-tap-highlight-color: transparent;
padding: 20rpx;
background: var(--system-gray-6);
color: var(--text-primary);
/* 按钮变体 */
&.btn-primary {
background: var(--apple-blue);
color: white;
}
&.btn-success {
background: var(--apple-green);
color: white;
}
&.btn-warning {
background: var(--apple-orange);
color: white;
}
&.btn-danger {
background: var(--apple-red);
color: white;
}
&.btn-secondary {
background: var(--system-gray-4);
color: var(--text-secondary);
}
/* 小号按钮 */
&.btn-mini {
min-height: 40rpx;
padding: 0 16rpx;
font-size: 24rpx;
border-radius: 8rpx;
}
/* 大号按钮 */
&.btn-large {
min-height: 64rpx;
padding: 0 32rpx;
font-size: 32rpx;
border-radius: 16rpx;
}
/* 禁用状态 */
&.btn-disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
/* 按钮组 */
.btn-group {
display: flex;
gap: 12rpx;
.btn-apple {
flex: 1;
}
&.btn-group-vertical {
flex-direction: column;
}
}
</style>

220
api/request.js Normal file
View File

@@ -0,0 +1,220 @@
/**
* 统一请求拦截器
*/
import {
userStore
} from '@/store/index.js';
// 基础配置
const BASE_CONFIG = {
timeout: 30000, // 30秒超时
header: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
};
/**
* 创建请求实例
*/
class HttpRequest {
constructor() {
this.config = {
...BASE_CONFIG
};
}
/**
* 响应拦截器 - 处理Cookie和错误
* @param {Object} response 响应对象
* @returns {Promise} 处理后的响应
*/
async interceptResponse(response) {
try {
if (response.data.system_result_message_key) {
uni.showToast({
title: response.data.system_result_message_key + ",请重新登录",
icon: 'none'
});
// 等待 logout 操作完成
await userStore.actions.logout();
setTimeout(() => {
uni.navigateTo({
url: '/pages/login/login'
});
}, 500);
}
// 检查HTTP状态码
if (response.statusCode >= 200 && response.statusCode < 300) {
// 检查业务状态码
if (response.data && typeof response.data === 'object') {
if (response.data.code === '0' || response.data.code === 0 || response.data.code === 200) {
// 业务成功
return response.data;
} else {
// 业务失败
const error = new Error(response.data.message || response.data.msg || '请求失败');
error.code = response.data.code;
error.data = response.data;
throw error;
}
} else {
// 非JSON响应直接返回
return response.data;
}
} else {
// HTTP状态码错误
const error = new Error(
`HTTP ${response.statusCode}: ${this.getStatusText(response.statusCode)}`
);
error.statusCode = response.statusCode;
error.response = response;
throw error;
}
} catch (error) {
console.error('响应拦截器错误:', error);
throw error;
}
}
/**
* 获取状态码描述
* @param {number} statusCode HTTP状态码
* @returns {string} 状态描述
*/
getStatusText(statusCode) {
const statusMap = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable'
};
return statusMap[statusCode] || 'Unknown Error';
}
/**
* 通用请求方法
* @param {Object} options 请求选项
* @returns {Promise} 请求Promise
*/
request(options) {
return new Promise((resolve, reject) => {
// 合并配置,特别处理 header
const config = {
...this.config,
...options,
header: {
...this.config.header,
...(options.header || options.headers || {})
},
withCredentials: false
};
// 发起请求
uni.request({
...config,
success: (response) => {
this.interceptResponse(response)
.then(resolve)
.catch(reject);
},
fail: (error) => {
console.error('请求失败:', error);
const err = new Error(error.errMsg || '网络请求失败');
err.error = error;
reject(err);
}
});
});
}
/**
* GET请求
* @param {string} url 请求地址
* @param {Object} params 查询参数
* @param {Object} options 其他选项
* @returns {Promise} 请求Promise
*/
get(url, params = {}, options = {}) {
// 构建查询字符串
const queryString = Object.keys(params)
.filter(key => params[key] !== undefined && params[key] !== null)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&');
const finalUrl = queryString ? `${url}${url.includes('?') ? '&' : '?'}${queryString}` : url;
return this.request({
url: finalUrl,
method: 'GET',
...options
});
}
/**
* POST请求
* @param {string} url 请求地址
* @param {Object} data 请求体数据
* @param {Object} options 其他选项
* @returns {Promise} 请求Promise
*/
post(url, data = {}, options = {}) {
return this.request({
url,
method: 'POST',
data,
...options
});
}
/**
* PUT请求
* @param {string} url 请求地址
* @param {Object} data 请求体数据
* @param {Object} options 其他选项
* @returns {Promise} 请求Promise
*/
put(url, data = {}, options = {}) {
return this.request({
url,
method: 'PUT',
data,
...options
});
}
/**
* DELETE请求
* @param {string} url 请求地址
* @param {Object} params 查询参数
* @param {Object} options 其他选项
* @returns {Promise} 请求Promise
*/
delete(url, params = {}, options = {}) {
return this.get(url, params, {
...options,
method: 'DELETE'
});
}
}
// 创建请求实例
const httpRequest = new HttpRequest();
export default httpRequest;
// 导出常用方法
export const {
get,
post,
put,
delete: del
} = httpRequest;

757
api/user.js Normal file
View File

@@ -0,0 +1,757 @@
/**
* 用户相关API接口
*/
import httpRequest from './request.js';
import {
generateRfm
} from '@/utils/common.js';
class UserApi {
constructor() {
this.baseUrl = '/kyhl-weixin-1.0';
this.cardBaseUrl = '/cm-api/v1'
}
// 检查是否有公众号OpenId获取Cookie的关键接口
async getCookie() {
try {
const fullUrl = `${this.cardBaseUrl}/auth/get-auth`;
const response = await httpRequest.post(fullUrl);
return response;
} catch (error) {
console.error('checkHasGzhOpenId请求失败:', error);
throw error;
}
}
// 获取设备信息(管理平台)
async getDeviceInfoAdmin(device_id) {
try {
const fullUrl = `${this.cardBaseUrl}/device/query`;
const response = await httpRequest.post(fullUrl, {
page: 1,
page_size: 1,
device_id
});
return response;
} catch (error) {
console.error('获取设备信息(管理平台):', error);
throw error;
}
}
// 获取WXUrl
/*
{
"code": "0",
"current_session_user_resource_ids_index": "",
"app_result_key": "0",
"wxAuthUrl": "",
"system_result_key": "0"
}
*/
async getWxUrl() {
try {
const params = {
responseFunction: 'checkHasGzhOpenId',
needGzhOpenId: true,
currentPageUrl: 'https://m1.whjhft.com/pages/index/index',
checkFrom: 'cardlogin',
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/wxauth/checkHasGzhOpenId.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('登录请求失败:', error);
throw error;
}
}
// 用户登录接口
async login(iccidMark) {
try {
console.log(iccidMark);
const params = {
responseFunction: 'findByiccidMarkCallback',
iccidMark,
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/card/login.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('登录请求失败:', error);
throw error;
}
}
// 判断是否实名接口
async getRealNameInfo() {
try {
const params = {
iccidOrPhone: "",
responseFunction: "getRealNameInfoCallback",
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/user/getRealNameInfo.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('获取实名信息失败:', error);
throw error;
}
}
// 获取用户信息接口
async getUserInfo() {
try {
const params = {
responseFunction: 'findByOpenIdCallback',
DoNotGetRealNameInfo: true,
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/user/findByOpenId.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('获取用户信息失败:', error);
throw error;
}
}
// 切换运营商 1: 电信 2: 联通 3: 移动 esim参数值
async changeOperator(esim, iccidMark) {
try {
const params = {
responseFunction: 'updateWifi',
optwifi: "esim",
esim,
iccidMark,
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/card/updateWifi.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('切换运营商失败:', error);
throw error;
}
}
// 进入实名是否只显示主卡
async getIsOnlyMainCard(iccidMark) {
try {
const fullUrl = `${this.cardBaseUrl}/call/device/${iccidMark}/exists`;
const response = await httpRequest.get(fullUrl);
return response;
} catch (error) {
console.error('进入实名是否只显示主卡请求失败:', error);
throw error;
}
}
// 获取openId接口
async getOpenId() {
try {
const params = {
responseFunction: 'getUserCardInfo',
DoNotGetRealNameInfo: true,
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/usercard/getUserCardInfo.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('获取openId失败:', error);
throw error;
}
}
// 进行授权
async getAuthorize(code) {
try {
const params = {
origin_html_url: 'https://m1.whjhft.com/pages/index/index',
code: code,
state: null
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/wxauth/recvCode.do?${queryString}`;
const response = await httpRequest.get(fullUrl);
return response;
} catch (error) {
console.error('获取授权失败:', error);
throw error;
}
}
// 获取微信支付签名
/*
{
"code": "0",
"signature": "3771f2fce5802b3630377e7d2618a7195d136007",
"current_session_user_resource_ids_index": "",
"appId": "wxea8c599fe100ce8a",
"nonceStr": "31565688940e48cb91827adbf6aeb499",
"system_result_key": "0",
"timestamp": 1766209145,
"isSuccess": "0"
}
*/
async getWxPaySign() {
try {
const fullUrl = `${this.baseUrl}/weixinPay/getWxSign.do`;
const response = await httpRequest.get(fullUrl);
return response;
} catch (error) {
console.error('获取微信支付签名失败:', error);
throw error;
}
}
getCurrentPageUrl() {
try {
// #ifdef H5
if (typeof window !== 'undefined' && window.location) {
const currentUrl = window.location.href;
console.log('当前页面URL:', currentUrl);
return encodeURIComponent(currentUrl);
}
// #endif
const defaultUrl = "https://m1.whjhft.com/pages/index/index";
console.log('使用默认URL:', defaultUrl);
return encodeURIComponent(defaultUrl);
} catch (error) {
console.error('获取当前页面URL失败:', error);
return encodeURIComponent('https://m1.whjhft.com/pages/index/index');
}
}
// 获取套餐列表 smList
async getPackageList() {
try {
const params = {
responseFunction: 'findByCardNoCallback',
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}//setmeal/findByCardNo.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('获取套餐列表失败:', error);
throw error;
}
}
// 1. checkHasGzhOpenId 判断当前用户是否已经绑定了微信公众号的 OpenId
/*
{
"code": "0",
"current_session_user_resource_ids_index": "",
"app_result_key": "0",
"wxAuthUrl": "",
"system_result_key": "0"
}
*/
// 如果返回的wxAuthUrl: 是空的就表示已经授权了, 没有就需要再来一次授权流程
async checkHasGzhOpenId() {
try {
const params = {
responseFunction: 'checkHasGzhOpenId',
needGzhOpenId: true,
currentPageUrl: "https://m1.whjhft.com/pages/package-order/package-order",
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}//wxauth/checkHasGzhOpenId.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('checkHasGzhOpenId:', error);
throw error;
}
}
// 2. checkYgosWxInfo 检查当前用户是否具有与微信支付相关的特定信息
async checkYgosWxInfo() {
try {
const params = {
responseFunction: 'checkYgosWxInfo',
hasYgGzhPm: false,
currentPageUrl: "https://m1.whjhft.com/pages/package-order/package-order",
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}//weixinPay/checkYgosWxInfo.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('checkYgosWxInfo:', error);
throw error;
}
}
// 3. checkXwzfWxInfo 检查微信信息
async checkXwzfWxInfo() {
try {
const params = {
responseFunction: 'checkXwzfWxInfo',
hasXwzfPm: false,
zwxMchId: "",
currentPageUrl: "https://m1.whjhft.com/pages/package-order/package-order",
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}//weixinPay/checkXwzfWxInfo.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('checkXwzfWxInfo:', error);
throw error;
}
}
// 4. 获取支付方式
/*
{
"onlyWalletRechargeCard": false,
"code": "0",
"walletBalance": 0.0,
"current_session_user_resource_ids_index": "",
"payMethodList": [
{
"mybatisRecordCount": 0,
"orderNo": "",
"jsonUpdateFlag": "0",
"id": "a333",
"createDate": "",
"createUserId": "",
"createUserName": "",
"payMethod": 2,
"showStatus": "",
"selectStatus": 2,
"sortNum": 44,
"showName": "余额支付",
"showIconUrl": "http://jh.whjhft.com/kyhl-weixin-1.0/img/yezf.png",
"appId": "",
"appSecret": "",
"gzhId": "",
"gzhName": "",
"gzhUsername": "",
"gzhPassword": "",
"mchId": "",
"mchKey": "",
"systemDomain": "",
"mchCertUrl": "",
"notifyUrl": "",
"returnUrl": "",
"profitSharingConfigNo": "",
"createDateStr": "",
"showApp": true,
"needOpenId": false,
"payMethodStr": "余额支付",
"showStatusStr": "",
"selectStatusStr": "否"
},
{
"mybatisRecordCount": 0,
"orderNo": "",
"jsonUpdateFlag": "0",
"id": "4F31156EE9654A35B16CF0E3A6569F53",
"createDate": "",
"createUserId": "",
"createUserName": "",
"payMethod": 1,
"showStatus": "",
"selectStatus": 1,
"sortNum": "",
"showName": "微信支付",
"showIconUrl": "http://jhft.whjhft.com/kyhl-weixin-1.0/img/wx.png",
"appId": "",
"appSecret": "",
"gzhId": "",
"gzhName": "",
"gzhUsername": "",
"gzhPassword": "",
"mchId": "",
"mchKey": "",
"systemDomain": "",
"mchCertUrl": "",
"notifyUrl": "",
"returnUrl": "",
"profitSharingConfigNo": "",
"createDateStr": "",
"showApp": false,
"needOpenId": true,
"payMethodStr": "微信支付",
"showStatusStr": "",
"selectStatusStr": "是"
}
],
"app_result_key": "0",
"buyMealMealPrice": 29.0,
"pwdEmpty": false,
"system_result_key": "0",
"isSuccess": "1"
}
*/
async getPayList(mealId) {
try {
const params = {
responseFunction: 'getNeedPayMoneyAndWalletBalance',
mealId,
getMealPrice: true,
allowWalletPay: true,
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}//cardwallet/getNeedPayMoneyAndWalletBalance.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('获取支付方式失败:', error);
throw error;
}
}
/*
{
"code": "0",
"current_session_user_resource_ids_index": "",
"wp": {
"appId": "wxea8c599fe100ce8a",
"timeStamp": "1766388518476",
"nonceStr": "1d64e63b37f349cfb3699643757268a3",
"prepayId": "prepay_id=wx2215283841316122e94a592ddd8e7f0000",
"paySign": "FDCD3D358E55C568725A10B4E7BC4F5B",
"signType": ""
},
"system_result_key": "0",
"isSuccess": "0"
}
*/
// 5. 调用 orderPayPageUse.do 创建订单 并拉起支付 我只要微信支付即可
async createOrder(data) {
try {
const fullUrl = `${this.baseUrl}/weixinPay/orderPayPageUse.do`;
// 将数据转换为 URL 编码格式
const p = new URLSearchParams(data).toString();
// 确保 header 中的 Content-Type 为 x-www-form-urlencoded
const response = await httpRequest.post(fullUrl, p, {
header: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
});
return response;
} catch (error) {
console.error('创建订单失败', error);
throw error;
}
}
// 获取切卡列表-设备流量-WIFI信息-连接数
async getSwitchCardList() {
try {
const params = {
responseFunction: 'findNetWorkInfo',
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/card/findCardMchInfo.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('获取切卡列表-设备流量-WIFI信息-连接数异常:', error);
throw error;
}
}
// 获取卡信息(过期时间expireDate-状态statusStr)
async getCardInfo() {
try {
const params = {
iccidOrPhone: '',
responseFunction: 'findCardInfoCallback',
skipGift: true,
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/card/findCardInfo.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('获取切卡列表-设备流量-WIFI信息-连接数异常:', error);
throw error;
}
}
// 修改WiFi信息
async modifyWifi({
cardNo,
ssid,
password
}) {
try {
const fullUrl = `${this.cardBaseUrl}/device/wifi-config`;
const response = await httpRequest.post(fullUrl, {
cardNo,
ssid,
password
});
return response;
} catch (error) {
console.error('修改WiFi信息失败:', error);
throw error;
}
}
// 重启设备
async restartDevice(deviceId) {
try {
const fullUrl = `${this.cardBaseUrl}/device/restart`;
const response = await httpRequest.post(fullUrl, {
cardNo: deviceId
});
return response;
} catch (error) {
console.error('重启设备失败:', error);
throw error;
}
}
// 恢复出厂设置
async restDevice(deviceId) {
try {
const fullUrl = `${this.cardBaseUrl}/device/factory-reset`;
const response = await httpRequest.post(fullUrl, {
cardNo: deviceId
});
return response;
} catch (error) {
console.error('恢复出厂设置失败:', error);
throw error;
}
}
// 获取实名地址
async getRealNameAddress(iccidMark) {
try {
const params = {
iccidOrPhone: "",
responseFunction: "getRealNameInfoCallback",
force: true,
iccidMark,
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/user/getRealNameInfo.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('获取实名地址异常:', error);
throw error;
}
}
// 智能诊断
/*
{
"internalRetMsg": "",
"code": "0",
"current_session_user_resource_ids_index": "",
"app_result_key": "0",
"retMsg": "已提交复机申请预计1小时内复机。",
"system_result_key": "0",
"isSuccess": "0"
}
*/
async intelligentDiagnosis(iccidMark) {
try {
const params = {
responseFunction: "intelliDiagnose"
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/card/intelliDiagnose.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('智能诊断异常:', error);
throw error;
}
}
// 获取设备绑定的手机号
async getDeviceBindPhone() {
try {
const params = {
responseFunction: 'findMyBindRecord',
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/phonebindrecord/findMyBindRecord.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('获取设备绑定的手机号异常:', error);
throw error;
}
}
// 获取短信验证码
async getSmsNumber(mobile) {
try {
const params = {
responseFunction: 'sendSms',
mobile,
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/phonebindrecord/sendSms.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('获取短信验证码异常:', error);
throw error;
}
}
// 绑定手机号
async bindCardPhone(mobile, code) {
try {
const params = {
responseFunction: 'saveBind',
mobile,
code,
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/phonebindrecord/saveBind1.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('绑定手机号异常:', error);
throw error;
}
}
// 退出登录
async logout() {
try {
const params = {
responseFunction: 'logoutCallback',
rfm: generateRfm()
};
const queryString = this.buildQueryString(params);
const fullUrl = `${this.baseUrl}/card/logout.do?${queryString}`;
const response = await httpRequest.post(fullUrl, null);
return response;
} catch (error) {
console.error('退出登录异常:', error);
throw error;
}
}
buildQueryString(params) {
return Object.keys(params)
.filter(key => params[key] !== undefined && params[key] !== null)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&');
}
}
const userApi = new UserApi();
export default userApi;

23
index.html Normal file
View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/misans@4.1.0/lib/Normal/MiSans-Regular.min.css"/>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

24
main.js Normal file
View File

@@ -0,0 +1,24 @@
import App from './App'
import uviewPlus from 'uview-plus'
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
app.use(uviewPlus)
return {
app
}
}
// #endif

101
manifest.json Normal file
View File

@@ -0,0 +1,101 @@
{
"name": "singer-card-h5",
"appid": "__UNI__45F0251",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
/* 5+App */
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
/* */
"modules": {},
/* */
"distribute": {
/* android */
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios": {},
/* SDK */
"sdkConfigs": {}
}
},
/* */
"quickapp": {},
/* */
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"h5": {
"title": "物联网卡查询",
"template": "index.html",
"router": {
"mode": "history",
"base": "/singer-card/"
},
"optimization": {
"treeShaking": {
"enable": true
}
},
"publicPath": "./singer-card/",
"devServer": {
"proxy": {
"/kyhl-weixin-1.0": {
"target": "http://jhwl.whjhft.com",
"changeOrigin": true,
"secure": false
},
"/cm-api": {
"target": "http://report.whjhft.com",
"changeOrigin": true,
"secure": false
}
}
},
"sdkConfigs": {}
}
}

1621
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

11
package.json Normal file
View File

@@ -0,0 +1,11 @@
{
"devDependencies": {
"sass": "^1.63.2",
"sass-loader": "^10.4.1"
},
"dependencies": {
"clipboard": "^2.0.11",
"dayjs": "^1.11.19",
"uview-plus": "^3.6.29"
}
}

61
pages.json Normal file
View File

@@ -0,0 +1,61 @@
{
"easycom": {
"autoscan": true,
// 注意一定要放在custom里否则无效https://ask.dcloud.net.cn/question/131175
"custom": {
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue"
}
},
"pages": [{
"path": "pages/login/login",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom"
}
},
{
"path": "pages/package-order/package-order",
"style": {
"navigationBarTitleText": "套餐列表"
}
},
{
"path": "pages/switch/switch",
"style": {
"navigationBarTitleText": "切换运营商"
}
},
{
"path": "pages/bind/bind",
"style": {
"navigationBarTitleText": "绑定手机号"
}
},
{
"path": "pages/auth/auth",
"style": {
"navigationBarTitleText": "实名认证"
}
},
{
"path": "pages/error/error",
"style": {
"navigationStyle": "custom"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "物联网卡查询",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

221
pages/auth/auth.vue Normal file
View File

@@ -0,0 +1,221 @@
<template>
<view class="container">
<view class="card" v-for="item in list">
<view class="flex-row-g20">
<view class="logo">
<image :src="getLogo(item.category).logo" mode="aspectFit"></image>
</view>
<view class="flex-col-g20">
<view class="iccid">
ICCID: {{item.iccidMark}}
</view>
<view class="operator">
运营商: {{getLogo(item.category).name}}
</view>
</view>
</view>
<view class="btn mt-30 flex-col-g20">
<up-button class="btn-apple btn-primary" v-if="item.isRealName" type="primary">
已实名
</up-button>
<up-button class="btn-apple btn-success" v-else type="success" @tap="toReal(item.iccidMark)">
去实名
</up-button>
</view>
</view>
</view>
</template>
<script setup>
import {
onMounted,
reactive,
ref,
computed
} from 'vue';
import {
userStore
} from '@/store/index.js';
let mchList = reactive([])
let list = reactive([])
let currentIccid = ref("")
let phone = ref("")
const device_id = uni.getStorageSync("device_id")
let opratorList = reactive([{
category: "124", // 中国电信
logo: "https://img2.baidu.com/it/u=139558247,3893370039&fm=253&fmt=auto?w=529&h=500",
esim: 1,
name: "中国电信"
},
{
category: "125", // 中国联通
logo: "https://img1.baidu.com/it/u=2816777816,1756344384&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500",
esim: 2,
name: "中国联通"
},
{
category: "126", // 中国移动
logo: "https://img2.baidu.com/it/u=915783975,1594870591&fm=253&fmt=auto&app=120&f=PNG?w=182&h=182",
esim: 3,
name: "中国移动"
}
])
const getLogo = computed(() => {
return (category) => {
const operator = opratorList.find(item => item.category == category);
return operator ? operator : ''; // 返回logo路径
};
});
// 是否显示主号 (导入的设备号)
const showMainNumber = async () => {
if (!device_id) {
uni.showToast({
title: 'ICCID不能为空',
icon: 'none'
})
}
const result = await userStore.actions.getIsOnlyMainCard(device_id)
if (result.data.exists && mchList.length > 0) {
// 使用 filter() 筛选符合条件的项
let matchedItems = mchList.filter(item => item.iccidMark === currentIccid.value);
// 将筛选后的数组重新赋值给 mchList
Object.assign(list, matchedItems)
} else {
Object.assign(list, mchList)
}
};
const toReal = async (iccid) => {
const result = await userStore.actions.getRealNameAddress(iccid)
let url = result.accountEntity.realNameUrl
if (url) {
// 替换url里面的 ${iccid} 替换为iccid ${phone}替换为 phone.value
url = url.replace("${iccid}", iccid).replace("${phone}", phone.value)
uni.showToast({
title: "正在跳转实名",
icon: "none"
})
window.location.href = url
} else {
uni.showToast({
title: '未获取到实名地址',
icon: 'none'
})
}
}
// 获取列表
const getList = async () => {
const mainInfo = await userStore.actions.getSwitchCardList()
if (mainInfo?.cardEntity) {
currentIccid.value = mainInfo.cardEntity.iccidMark
phone.value = mainInfo.cardEntity.phone
}
if (mainInfo?.mchList.length > 0) {
Object.assign(mchList, mainInfo.mchList)
}
}
onMounted(async () => {
await getList()
await showMainNumber()
})
</script>
<style lang="scss" scoped>
.container {
.card {
.logo {
width: 80rpx;
height: 80rpx;
border-radius: 120rpx;
border: 1rpx solid var(--apple-blue);
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.name {
font-size: 30rpx;
}
}
}
.btn-apple {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 12rpx;
font-weight: 500;
font-size: 26rpx;
line-height: 1;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
-webkit-tap-highlight-color: transparent;
padding: 20rpx;
background: var(--system-gray-6);
color: var(--text-primary);
/* 按钮变体 */
&.btn-primary {
background: var(--apple-blue);
color: white;
}
&.btn-success {
background: var(--apple-green);
color: white;
}
&.btn-warning {
background: var(--apple-orange);
color: white;
}
&.btn-danger {
background: var(--apple-red);
color: white;
}
&.btn-secondary {
background: var(--system-gray-4);
color: var(--text-secondary);
}
/* 小号按钮 */
&.btn-mini {
min-height: 40rpx;
padding: 0 16rpx;
font-size: 24rpx;
border-radius: 8rpx;
}
/* 大号按钮 */
&.btn-large {
min-height: 64rpx;
padding: 0 32rpx;
font-size: 32rpx;
border-radius: 16rpx;
}
/* 禁用状态 */
&.btn-disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
</style>

148
pages/bind/bind.vue Normal file
View File

@@ -0,0 +1,148 @@
<template>
<view class="container">
<view v-if="isBind" class="card flex-col-g20">
<view class="flex-row-g20">
<label for="">手机号:</label>
<up-input placeholder="请输入绑定的手机号" border="surround" v-model="bind.mobile" />
</view>
<view class="flex-row-g20">
<label for="">验证码:</label>
<up-input placeholder="验证码" v-model="bind.code">
<template #suffix>
<up-button @tap="getCode" text="获取验证码" type="success"></up-button>
</template>
</up-input>
</view>
<view class="btn">
<up-button type="primary" @tap="bindPhone">绑定手机号</up-button>
</view>
</view>
<view class="card" v-else>
<up-cell-group>
<up-cell title="绑定手机号" :value="bindInfo.phone"></up-cell>
<up-cell title="绑定时间" :value="bindInfo.bindTime"></up-cell>
<up-cell title="ICCID" :value="bindInfo.iccid"></up-cell>
<up-cell title="状态" :value="bindInfo.status"></up-cell>
</up-cell-group>
<view class="btn mt-30 flex-col-g20">
<up-button type="primary" @tap="changeBind">更换绑定</up-button>
<up-button type="success" @tap="logout">退出</up-button>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
reactive
} from 'vue'
import {
userStore
} from '@/store/index.js';
let isBind = ref(false)
let bind = reactive({
mobile: "",
code: ""
})
let bindInfo = reactive({
phone: "",
iccid: "",
bindTime: "",
status: 0
})
const changeBind = () => {
isBind.value = true
}
// 获取绑定信息
const getBindInfo = async () => {
const result = await userStore.actions.getDeviceBindPhone()
if (result.myBindRecord.bindPhone) {
isBind.value = false
bindInfo.phone = result.myBindRecord.bindPhone
bindInfo.iccid = result.myBindRecord.iccidMark
bindInfo.bindTime = result.myBindRecord.createDateStr
bindInfo.status = result.myBindRecord.status === 1 ? "已绑定" : "未绑定"
} else {
isBind.value = true
}
}
// 获取验证码
const getCode = async () => {
if (!bind.mobile) {
uni.showToast({
title: "请输入手机号",
icon: "none"
})
return
}
const data = await userStore.actions.getSmsNumber(bind.mobile)
if (data.isSuccess && data?.errorMsg) {
uni.showToast({
title: data.errorMsg,
icon: "none"
})
} else {
uni.showToast({
title: "验证码已发送",
icon: "none"
})
}
}
// 绑定手机号
const bindPhone = async () => {
if (!bind.mobile && !bind.code) {
uni.showToast({
title: "手机号和验证码都不能为空",
icon: "none"
})
return
}
const data = await userStore.actions.bindCardPhone(bind.mobile, bind.code)
if (data.isSuccess && data?.errorMsg) {
uni.showToast({
title: data.errorMsg,
icon: "none"
})
return
} else {
uni.showToast({
title: "绑定成功",
icon: "none"
})
}
setTimeout(() => {
isBind.value = false
getBindInfo()
})
}
// 退出
const logout = async() => {
await userStore.actions.logout()
uni.navigateTo({
url: "/pages/login/login"
})
}
onMounted(() => {
getBindInfo()
})
</script>
<style>
</style>

21
pages/error/error.vue Normal file
View File

@@ -0,0 +1,21 @@
<template>
<view class="error">
请在微信中打开...
</view>
</template>
<script setup>
</script>
<style lang="less" scoped>
.error{
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 30rpx;
color: #999;
}
</style>

1102
pages/index/index.vue Normal file

File diff suppressed because it is too large Load Diff

109
pages/login/login.vue Normal file
View File

@@ -0,0 +1,109 @@
<template>
<view class="container">
<view class="card">
<view class="title-login">
物联网卡登录
</view>
<view class="input">
<up-input class="mt-30" v-model="device_id" placeholder="请输入ICCID"></up-input>
</view>
<view class="button">
<up-button class="mt-30 btn-apple btn-primary" type=" primary" @click="login">立即登录</up-button>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
reactive,
onMounted,
computed
} from 'vue';
import {
userStore
} from '@/store/index.js';
const device_id = ref('');
const login = async () => {
if (!device_id.value) {
uni.showToast({
title: '请输入ICCID',
icon: 'none'
})
return
}
try {
const result = await userStore.actions.login(device_id.value);
if (result.system_result_message_key) {
uni.showToast({
title: result.system_result_message_key,
icon: 'none'
})
return
}
uni.showToast({
title: '登录成功',
icon: 'none'
})
// 登录成功后跳转到首页登录状态已在store中设置
uni.redirectTo({
url: "/pages/index/index"
})
} catch (error) {
console.error('登录过程中发生错误:', error);
uni.showToast({
title: '登录失败,请稍后重试',
icon: 'none'
})
}
}
// 获取扫码后路由中带的device_id
const getPathDeviceId = () => {
const path = window.location.href.split("=")
if (path.length > 1) {
device_id.value = path[1]
login()
}
}
// 进入页面后自动获取Cookie
const getCookieAndWxUrl = async () => {
await userStore.actions.getWxUrl();
}
onMounted(() => {
getCookieAndWxUrl()
getPathDeviceId()
})
</script>
<style lang="scss" scoped>
.container {
padding-top: 30vh;
.title-login {
color: #333;
font-size: 24px;
text-align: center;
margin: 20px 0;
font-weight: bold;
}
.input {
margin: 70rpx 0 50rpx 0;
}
.button {
margin-bottom: 40rpx;
}
}
</style>

View File

@@ -0,0 +1,301 @@
<template>
<view class="container">
<view v-if="packageList.length > 0" class="card flex-col-g20" v-for="item in packageList">
<view class="title">
{{item.name}}
</view>
<view class="price">
{{item.accountMoney}}
</view>
<view class="desc">
请在套餐有效期内使用,有效期内流量用完可充值加餐包即可继续使用
</view>
<view class="btn">
<up-button type="primary" @tap="showBuy=true">立即订购</up-button>
</view>
<up-modal :title="`您确定要订购${item.name}套餐吗?`" :show="showBuy" showCancelButton @confirm="SubscribeNow(item)"
@cancel="showBuy=false" />
</view>
<view v-else class="card">
<view class="title" style="text-align: center;">
该设备暂无套餐, 请联系客服购买
</view>
</view>
</view>
</template>
<script setup>
import {
onMounted,
reactive,
ref
} from 'vue';
import {
userStore
} from '@/store/index.js';
let showBuy = ref(false)
// 套餐列表
let packageList = reactive([]);
// 支付列表
let payList = reactive([]);
// 获取套餐列表
const getPackageList = async () => {
const result = await userStore.actions.getPackageList()
if (result.smList.length > 0) {
packageList.push(...result.smList)
}
}
// 立即订购
const SubscribeNow = async (item) => {
// 1. 判断当前用户是否已经绑定了微信公众号的 OpenId
await checkHasGzhOpenId()
// 2. checkYgosWxInfo 检查当前用户是否具有与微信支付相关的特定信息
await userStore.actions.checkYgosWxInfo()
// 3. checkXwzfWxInfo 检查当前用户是否具有与微信支付相关的特定信息
await userStore.actions.checkXwzfWxInfo()
// 4. 获取支付列表
await getPayList(item.id)
let id = "";
if (payList.length === 2) {
id = payList[1].id
} else {
uni.showToast({
title: "未找到微信支付",
icon: "none"
})
}
const params = {
mealId: item.id,
cardID: "",
money: item.money,
mealType: item.type,
cardCount: item.cardCount,
mealName: item.name,
strEffectType: "-1",
__pay_method: "1",
__merchant_cfg_id: id,
payPwd: ""
}
// 5. 创建订单, 获取支付参数
await createOrder(params)
}
// 1. 判断当前用户是否已经绑定了微信公众号的 OpenId
const checkHasGzhOpenId = async () => {
const result = await userStore.actions.checkHasGzhOpenId()
if (result.wxAuthUrl) {
// 再次进行授权
uni.showToast({
title: "该用户需要再次授权",
icon: "none"
})
toAuth()
}
}
// 4. 获取支付列表
const getPayList = async (mealId) => {
const result = await userStore.actions.getPayList(mealId)
if (result.payMethodList.length > 0) {
Object.assign(payList, result.payMethodList)
} else {
toAuth()
}
}
// 5. 创建订单, 获取支付参数, 并拉起微信支付
const createOrder = async (params) => {
showBuy.value = false;
try {
// 调用后端接口,获取支付参数
const data = await userStore.actions.createOrder(params);
// 如果有错误信息,显示错误提示
if (data?.errorMsg) {
uni.showToast({
title: data?.errorMsg,
icon: "none"
});
return;
}
// 调起微信支付
if (data?.wp) {
await invokeWechatPay(data?.wp);
} else {
uni.showToast({
title: '获取支付参数失败',
icon: 'none'
});
}
} catch (error) {
uni.showToast({
title: "拉起微信支付失败",
icon: "none"
})
}
};
// 调起微信支付
const invokeWechatPay = (payParam) => {
return new Promise((resolve, reject) => {
console.log('[PackageOrder] 调起微信支付...', payParam)
const onBridgeReady = () => {
window.WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
appId: payParam.appId,
timeStamp: payParam.timeStamp,
nonceStr: payParam.nonceStr,
package: payParam.prepayId,
signType: payParam.signType || 'MD5',
paySign: payParam.paySign
},
(res) => {
console.log('[PackageOrder] 微信支付结果:', res)
if (res.err_msg === 'get_brand_wcpay_request:ok') {
resolve(res)
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
reject(new Error('用户取消支付'))
} else {
reject(new Error('支付失败: ' + res.err_msg))
}
}
)
}
if (typeof window.WeixinJSBridge === 'undefined') {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false)
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady)
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady)
}
} else {
onBridgeReady()
}
})
}
// 跳转到微信授权页面
const toAuth = async () => {
try {
const result = await userStore.actions.getWxUrl();
if (result.wxAuthUrl) {
// 获取当前页面的URL
const currentUrl = window.location.href;
// 检查当前URL是否已经包含 code 参数
if (!currentUrl.includes("code=")) {
// 如果没有code参数跳转到微信授权页面
let originalUrl = result.wxAuthUrl;
// 目标替换的 redirect_uri
const newRedirectUri = "https://m1.whjhft.com/pages/index/index";
// 替换 "https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxea8c599fe100ce8a&redirect_uri=https://m1.whjhft.com/my/my&response_type=code&scope=snsapi_userinfo&state=cardlogin#wechat_redirect"
// 使用正则表达式替换 redirect_uri 的值
const updatedUrl = originalUrl.replace(/(redirect_uri=)[^&]*/,
`$1${encodeURIComponent(newRedirectUri)}`);
if (updatedUrl) {
window.location.href = updatedUrl;
}
} else {
// 如果已经包含code参数, 就进行授权
authoration()
}
} else {
uni.showToast({
title: '该用户已授权',
icon: 'none'
})
}
} catch (e) {
uni.showToast({
title: e,
icon: 'none'
});
}
};
// 授权
const authoration = async () => {
try {
// 获取当前页面的完整 URL https://m1.whjhft.com/pages/index/index?code=081KSnml23G2Ug4XViml2VFHyJ0KSnmF&state=cardlogin
// const url = window.location.href;
const code = (location.search.match(/[?&]code=([^&]*)/) || [])[1];
if (code) {
// 如果 code 存在,则调用获取授权的动作
await userStore.actions.getAuthorize(code);
removeCodeFromUrl()
} else {
// 如果 code 不存在
uni.showToast({
title: "微信授权失败!",
icon: "none"
});
}
} catch (error) {
// 错误处理
console.error('授权失败:', error);
uni.showToast({
title: "授权失败,请重试",
icon: "none"
});
}
};
onMounted(() => {
getPackageList()
})
</script>
<style lang="scss" scoped>
.container {
.card {
.title {
font-size: 32rpx;
font-weight: 600;
}
.price {
font-size: 32rpx;
font-weight: bold;
color: #aa5500;
}
.btn {
margin-top: 20rpx;
}
}
.container {
.pay-title {
font-size: 35rpx;
text-align: center;
}
}
}
</style>

234
pages/switch/switch.vue Normal file
View File

@@ -0,0 +1,234 @@
<template>
<view class="container">
<view class="card" v-for="item in mchList">
<view class="flex-row-sb mt-30">
<view class="flex-row-g20">
<view class="logo">
<image :src="getLogo(item.category).logo" mode="aspectFit"></image>
</view>
<view class="flex-col-g20">
<view class="flex-row-g20">
<view class="iccid">
ICCID: {{item.iccidMark}}
</view>
<view class="operator">
<up-tag type="success"
size="mini">{{getLogo(item.category).name}}</up-tag>
</view>
</view>
<view class="flex-row-g20">
<view class="operator" v-if="item.iccidMark===currentIccid">
<up-tag type="success"
size="mini">{{item.iccidMark===currentIccid ? "当前使用" : ""}}</up-tag>
</view>
<view class="operator" v-if="item.iccidMark===currentIccidMain">
<up-tag type="success"
size="mini">{{item.iccidMark===currentIccidMain ? "当前主卡" : ""}}</up-tag>
</view>
<view class="operator">
<up-tag :type="item.isRealName ? 'primary': 'success'"
size="mini">{{item.isRealName ? "已实名" : "未实名"}}</up-tag>
</view>
</view>
</view>
</view>
</view>
<view class="btn flex-col-g20 mt-30">
<up-button class="btn-apple btn-success" v-if="item.iccidMark!==currentIccid" type="success"
@tap="switchShow=true">
切换此运营商
</up-button>
<up-button class="btn-apple btn-success" v-if="!item.isRealName" type="success" @tap="goToReal">
跳转实名页面
</up-button>
</view>
<!-- 切换运营商 -->
<up-modal title="您确定要切换运营商吗?" :show="switchShow" showCancelButton @confirm="changeOpera(item)"
@cancel="switchShow=false" />
</view>
</view>
</template>
<script setup>
import {
onMounted,
reactive,
ref,
computed
} from 'vue';
import {
onLoad
} from '@dcloudio/uni-app'
import {
userStore
} from '@/store/index.js';
let mchList = reactive([])
// 当前主卡
let currentIccidMain = ref("")
// 当前卡
let currentIccid = ref("")
// 是否显示
let switchShow = ref(false)
let opratorList = reactive([{
category: "124", // 中国电信
logo: "https://img2.baidu.com/it/u=139558247,3893370039&fm=253&fmt=auto?w=529&h=500",
esim: 1,
name: "中国电信"
},
{
category: "125", // 中国联通
logo: "https://img1.baidu.com/it/u=2816777816,1756344384&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500",
esim: 2,
name: "中国联通"
},
{
category: "126", // 中国移动
logo: "https://img2.baidu.com/it/u=915783975,1594870591&fm=253&fmt=auto&app=120&f=PNG?w=182&h=182",
esim: 3,
name: "中国移动"
}
])
const getLogo = computed(() => {
return (category) => {
const operator = opratorList.find(item => item.category == category);
return operator ? operator : ''; // 返回logo路径
};
});
const changeOpera = async (data) => {
let matchedItem = opratorList.find(item => item.category === data.category);
await userStore.actions.changeOperator(matchedItem.esim, data.iccidMark)
uni.showToast({
title: "切换成功, 3-5分钟后生效!",
icon: "none"
})
switchShow.value = false
}
const goToReal = () => {
uni.navigateTo({
url: "/pages/auth/auth"
})
}
// 获取列表
const getList = async () => {
const mainInfo = await userStore.actions.getSwitchCardList()
if (mainInfo?.cardEntity) {
// 当前主卡
currentIccidMain.value = mainInfo.cardEntity.iccidMark
// 当前使用卡
currentIccid.value = mainInfo.gswlinfo.iccid
}
if (mainInfo?.mchList.length > 0) {
Object.assign(mchList, mainInfo.mchList)
}
}
const YesSwitch = async () => {
switchShow.value = true
}
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.container {
.card {
.logo {
width: 80rpx;
height: 80rpx;
border-radius: 120rpx;
border: 1rpx solid var(--apple-blue);
overflow: hidden;
image {
width: 100%;
height: 100%;
}
}
.name {
font-size: 30rpx;
}
}
}
.btn-apple {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 12rpx;
font-weight: 500;
font-size: 26rpx;
line-height: 1;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
-webkit-tap-highlight-color: transparent;
padding: 20rpx;
background: var(--system-gray-6);
color: var(--text-primary);
/* 按钮变体 */
&.btn-primary {
background: var(--apple-blue);
color: white;
}
&.btn-success {
background: var(--apple-green);
color: white;
}
&.btn-warning {
background: var(--apple-orange);
color: white;
}
&.btn-danger {
background: var(--apple-red);
color: white;
}
&.btn-secondary {
background: var(--system-gray-4);
color: var(--text-secondary);
}
/* 小号按钮 */
&.btn-mini {
min-height: 40rpx;
padding: 0 16rpx;
font-size: 24rpx;
border-radius: 8rpx;
}
/* 大号按钮 */
&.btn-large {
min-height: 64rpx;
padding: 0 32rpx;
font-size: 32rpx;
border-radius: 16rpx;
}
/* 禁用状态 */
&.btn-disabled {
opacity: 0.4;
cursor: not-allowed;
}
}
</style>

BIN
static/authentication.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

BIN
static/back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
static/bind-phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

BIN
static/change.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
static/diagnosis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

BIN
static/link.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
static/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
static/out.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

BIN
static/recover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
static/restart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

BIN
static/shop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

15
store/index.js Normal file
View File

@@ -0,0 +1,15 @@
/**
* 简单的响应式状态管理
* 基于 Vue 3 reactive API
*/
import { reactive, ref } from 'vue';
import userStore from './modules/user.js';
// 创建全局状态
const globalStore = reactive({
user: userStore
});
export default globalStore;
export { userStore };

435
store/modules/user.js Normal file
View File

@@ -0,0 +1,435 @@
/**
* 用户状态管理模块
*/
import {
reactive,
computed
} from 'vue';
import userApi from '@/api/user.js';
// 用户状态数据
const state = reactive({
userInfo: {
nickname: '单卡用户', // 昵称
avatar: 'https://img1.baidu.com/it/u=2462918877,1866131262&fm=253&fmt=auto&app=138&f=JPEG?w=506&h=500', // 头像
deviceId: '' // 设备ID
},
// 用户信息
isLoggedIn: false, // 登录状态
error: null // 错误信息
});
// 计算属性
const getters = {
// 是否已登录
isLoggedIn: computed(() => state.isLoggedIn && !!state.userInfo.deviceId),
// 用户信息
userInfo: computed(() => state.userInfo)
};
// 操作方法
const actions = {
// 获取Cookies
async getCookie() {
try {
return userApi.getCookie();
} catch (error) {
console.error('获取微信授权失败:', error);
state.error = error.message;
throw error;
}
},
// 登录
async login(iccidMark) {
try {
// 调用login接口
const result = await userApi.login(iccidMark);
// 如果登录成功,设置登录状态和用户信息
if (!result.system_result_message_key) {
state.isLoggedIn = true;
state.userInfo.deviceId = iccidMark;
// 同步到本地存储
uni.setStorageSync('device_id', iccidMark);
uni.setStorageSync('isLoggedIn', true);
console.log('登录成功,已设置登录状态');
}
return result;
} catch (error) {
console.error('登录失败:', error);
state.error = error.message;
state.isLoggedIn = false;
throw error;
}
},
// 获取用户实名信息
async getRealNameInfo() {
try {
return userApi.getRealNameInfo();
} catch (error) {
console.error('获取用户实名信息失败:', error);
state.error = error.message;
throw error;
}
},
// 获取用户信息
async getUserInfo() {
try {
const result = await userApi.getUserInfo();
if (result?.entity) {
state.userInfo.avatar = result.entity.headimgurl
state.userInfo.nickname = result.entity.nickname
} else {
const result_once = await userApi.getUserInfo();
if (result_once?.entity) {
state.userInfo.avatar = result_once.entity.headimgurl
state.userInfo.nickname = result_once.entity.nickname
}
}
return result
} catch (error) {
console.error('获取用户信息失败:', error);
state.error = error.message;
throw error;
}
},
// 获取wxUrl
async getWxUrl() {
try {
return userApi.getWxUrl();
} catch (error) {
console.error('获取获取wxUrl失败:', error);
state.error = error.message;
throw error;
}
},
// 获取getOpenId
async getOpenId() {
try {
return userApi.getOpenId();
} catch (error) {
console.error('获取获取getOpenId失败:', error);
state.error = error.message;
throw error;
}
},
// 微信授权
async getAuthorize(code) {
try {
return userApi.getAuthorize(code);
} catch (error) {
console.error('获取微信授权失败:', error);
state.error = error.message;
throw error;
}
},
// 获取微信支付签名
async getWxPaySign() {
try {
return userApi.getWxPaySign();
} catch (error) {
console.error('获取微信支付签名失败:', error);
state.error = error.message;
throw error;
}
},
// 恢复出厂设置
async restDevice(deviceId) {
try {
return userApi.restDevice(deviceId);
} catch (error) {
console.error('恢复出厂设置失败:', error);
state.error = error.message;
throw error;
}
},
// 重启设备
async restartDevice(deviceId) {
try {
return userApi.restartDevice(deviceId);
} catch (error) {
console.error('重启设备失败:', error);
state.error = error.message;
throw error;
}
},
// 获取设备信息
async getCardInfo() {
try {
return userApi.getCardInfo();
} catch (error) {
console.error('获取设备信息失败:', error);
state.error = error.message;
throw error;
}
},
// 获取设备信息(管理平台)
async getDeviceInfoAdmin(device_id) {
try {
return userApi.getDeviceInfoAdmin(device_id);
} catch (error) {
console.error('获取设备信息(管理平台)', error);
state.error = error.message;
throw error;
}
},
// 获取首页信息-设备列表
async getSwitchCardList() {
try {
return userApi.getSwitchCardList();
} catch (error) {
console.error('获取首页信息-设备列表失败:', error);
state.error = error.message;
throw error;
}
},
// 修改WiFi
async modifyWifi(data) {
try {
return userApi.modifyWifi(data);
} catch (error) {
console.error('修改WiFi失败:', error);
state.error = error.message;
throw error;
}
},
// 获取套餐列表
async getPackageList() {
try {
return userApi.getPackageList();
} catch (error) {
console.error('获取套餐列表失败:', error);
state.error = error.message;
throw error;
}
},
// 智能诊断
async intelligentDiagnosis() {
try {
return userApi.intelligentDiagnosis();
} catch (error) {
console.error('智能诊断失败:', error);
state.error = error.message;
throw error;
}
},
// 获取绑定手机号
async getDeviceBindPhone() {
try {
return userApi.getDeviceBindPhone();
} catch (error) {
console.error('获取绑定手机号失败:', error);
state.error = error.message;
throw error;
}
},
// 获取验证码
async getSmsNumber(mobile) {
try {
return userApi.getSmsNumber(mobile);
} catch (error) {
console.error('获取验证码失败:', error);
state.error = error.message;
throw error;
}
},
// 绑定手机号
async bindCardPhone(mobile, code) {
try {
return userApi.bindCardPhone(mobile, code);
} catch (error) {
console.error('绑定手机号失败:', error);
state.error = error.message;
throw error;
}
},
// 是否显示主号
async getIsOnlyMainCard(iccid) {
try {
return userApi.getIsOnlyMainCard(iccid);
} catch (error) {
console.error('是否显示主号失败:', error);
state.error = error.message;
throw error;
}
},
// 获取实名地址
async getRealNameAddress(iccid) {
try {
return userApi.getRealNameAddress(iccid);
} catch (error) {
console.error('获取实名地址失败:', error);
state.error = error.message;
throw error;
}
},
// 切换运营商
async changeOperator(esim, iccid) {
try {
return userApi.changeOperator(esim, iccid);
} catch (error) {
console.error('切换运营商失败:', error);
state.error = error.message;
throw error;
}
},
// 1. checkHasGzhOpenId 判断当前用户是否已经绑定了微信公众号的
async checkHasGzhOpenId() {
try {
return userApi.checkHasGzhOpenId();
} catch (error) {
console.error('checkHasGzhOpenId 失败:', error);
state.error = error.message;
throw error;
}
},
// 2. checkYgosWxInfo 检查当前用户是否具有与微信支付相关的特定信息
async checkYgosWxInfo() {
try {
return userApi.checkYgosWxInfo();
} catch (error) {
console.error('checkYgosWxInfo 失败:', error);
state.error = error.message;
throw error;
}
},
// 3. checkXwzfWxInfo 检查当前用户是否具有与微信支付相关的特定信息
async checkXwzfWxInfo() {
try {
return userApi.checkXwzfWxInfo();
} catch (error) {
console.error('checkXwzfWxInfo 失败:', error);
state.error = error.message;
throw error;
}
},
// 4. 获取支付列表
async getPayList(mealId) {
try {
return userApi.getPayList(mealId);
} catch (error) {
console.error('获取支付列表失败:', error);
state.error = error.message;
throw error;
}
},
// 5. createOrder 创建订单 并拉起支付 这个会返回微信支付参数
async createOrder(data) {
try {
return userApi.createOrder(data);
} catch (error) {
console.error('createOrder 失败:', error);
state.error = error.message;
throw error;
}
},
// 登出清除Cookie和状态
async logout() {
try {
await userApi.logout();
// 重置状态
state.isLoggedIn = false;
state.userInfo = {
nickname: '',
avatar: '',
deviceId: ''
};
state.error = null;
// 清除本地存储
uni.removeStorageSync('device_id');
uni.removeStorageSync('isLoggedIn');
console.log('退出登录成功,已清除登录状态');
// 跳转到登录页
uni.redirectTo({
url: '/pages/login/login'
});
} catch (error) {
console.error('登出失败:', error);
state.error = error.message;
// 即使服务端退出失败,也要清除本地状态
state.isLoggedIn = false;
state.userInfo = {
nickname: '',
avatar: '',
deviceId: ''
};
uni.removeStorageSync('device_id');
uni.removeStorageSync('isLoggedIn');
// 跳转到登录页
uni.redirectTo({
url: '/pages/login/login'
});
}
},
// 重置错误状态
clearError() {
state.error = null;
},
// 初始化登录状态(从本地存储恢复)
initLoginState() {
try {
const deviceId = uni.getStorageSync('device_id');
const isLoggedIn = uni.getStorageSync('isLoggedIn');
if (deviceId && isLoggedIn) {
state.isLoggedIn = true;
state.userInfo.deviceId = deviceId;
console.log('从本地存储恢复登录状态:', deviceId);
} else {
state.isLoggedIn = false;
state.userInfo.deviceId = '';
console.log('未找到有效的登录状态');
}
} catch (error) {
console.error('初始化登录状态失败:', error);
state.isLoggedIn = false;
state.userInfo.deviceId = '';
}
},
};
// 创建用户store
const userStore = {
state,
getters,
actions
};
export default userStore;

15
tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"lib": ["esnext", "dom"],
"types": [
"@dcloudio/types",
"uview-plus/types"
]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

13
uni.promisify.adaptor.js Normal file
View File

@@ -0,0 +1,13 @@
uni.addInterceptor({
returnValue (res) {
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => {
if (!res) return resolve(res)
return res[0] ? reject(res[0]) : resolve(res[1])
});
});
},
});

76
uni.scss Normal file
View File

@@ -0,0 +1,76 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
@import 'uview-plus/theme.scss';
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;

92
utils/common.js Normal file
View File

@@ -0,0 +1,92 @@
/**
* 公共工具函数
*/
/**
* 生成随机浮点数参数rfm
* 用于接口请求的防重复参数
* @returns {number} 0到1之间的随机浮点数
*/
export function generateRfm() {
return Math.random();
}
/**
* 获取当前时间戳
* @returns {number} 当前时间戳
*/
export function getTimestamp() {
return Date.now();
}
/**
* 生成带时间戳的随机参数
* @returns {number} 带时间戳的随机数
*/
export function generateTimestampRfm() {
return Math.random() + Date.now() / 1000000;
}
/**
* 构建通用请求参数
* @param {Object} customParams - 自定义参数
* @returns {Object} 包含通用参数的对象
*/
export function buildCommonParams(customParams = {}) {
return {
rfm: generateRfm(),
...customParams
};
}
/*
* 转换成GB
* */
export function convertToGB(value, type) {
if (type === 'flowSize') {
// flowSize 单位是 MB转换为 GB
return (value / 1024).toFixed(2);
} else if (type === 'totalBytesCnt') {
// totalBytesCnt 单位是 MB转换为 GB
return (value / 1024).toFixed(2);
}
return value;
}
/**
* 秒转时间字符串
* @param {number} seconds 秒
* @returns {string} HH:mm:ss
*/
export function formatSecondsToTime(seconds) {
if (!seconds && seconds !== 0) return '00:00:00';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
return (
String(h).padStart(2, '0') + ':' +
String(m).padStart(2, '0') + ':' +
String(s).padStart(2, '0')
);
}
/**
* 根据信号强度(dBm)返回信号等级与体验描述
* @param {number} dbm 信号强度(负数,如 -65
* @returns {string}
*/
export function getSignalText(dbm) {
if (!dbm) {
return '未知'
}
if (dbm >= -50) {
return '极强'
}
if (dbm >= -70) {
return '良好'
}
return '一般'
}