Compare commits

...

3 Commits
master ... main

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
/unpackage/*

@ -0,0 +1,16 @@
{
// launch.json configurations app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
// launchtypelocalremote, localremote
"version": "0.0",
"configurations": [
{
"app-plus": {
"launchtype": "local"
},
"default": {
"launchtype": "local"
},
"type": "uniCloud"
}
]
}

@ -0,0 +1,198 @@
<script>
import apiAuth from "@/api/auth.js";
import apiStations from "@/api/stations";
export default {
//
onLaunch: function () {
console.log("App Launch");
// token
const authToken = uni.getStorageSync("auth-token");
if (authToken) {
apiAuth
.detail({ authToken })
.then((res) => {
this.$store.dispatch("updateAuth", { ...res.data, token: authToken });
})
.catch(() => {
uni.removeStorageSync("auth-token");
});
}
//
apiStations
.get()
.then((res) => this.$store.dispatch("updateStations", res.data))
.catch(() => {});
},
onShow: function () {
console.log("App Show");
},
onHide: function () {
console.log("App Hide");
},
};
</script>
<style lang="scss">
@import "uview-ui/index.scss";
@import "uview-ui/libs/css/flex.scss";
.u-flex-shrink-0 {
flex-shrink: 0;
}
/* 间距 $spacers 在 uni.scss 中 */
@each $key, $val in $spacers {
.mt-#{$key},
.my-#{$key},
.m-#{$key} {
margin-top: $val;
}
.mb-#{$key},
.my-#{$key},
.m-#{$key} {
margin-bottom: $val;
}
.ms-#{$key},
.mx-#{$key},
.m-#{$key} {
margin-left: $val;
}
.me-#{$key},
.mx-#{$key},
.m-#{$key} {
margin-right: $val;
}
.pt-#{$key},
.py-#{$key},
.p-#{$key} {
padding-top: $val;
}
.pb-#{$key},
.py-#{$key},
.p-#{$key} {
padding-bottom: $val;
}
.ps-#{$key},
.px-#{$key},
.p-#{$key} {
padding-left: $val;
}
.pe-#{$key},
.px-#{$key},
.p-#{$key} {
padding-right: $val;
}
.rounded-#{$key} {
border-radius: $val;
}
}
:root {
@each $key, $val in $spacers {
--space-#{$key}: #{$val};
--rounded-#{$key}: #{$val};
}
}
/* 颜色 $colors 在 uni.scss 中 */
@each $key, $val in $colors {
.text-#{$key} {
color: $val;
}
}
:root {
@each $key, $val in $colors {
--color-#{$key}: #{$val};
}
}
/* 每个页面公共css */
page {
background-color: $u-bg-color; // uview-ui/theme.scss
}
.p-page {
@extend .p-base;
}
.page-box-x-overflow {
margin-left: calc(var(--space-base) * -1);
margin-right: calc(var(--space-base) * -1);
}
.page-box-t-overflow {
margin-top: calc(var(--space-base) * -1);
}
.page-box {
@extend .bg-white, .rounded-sm;
}
.page-box + .page-box {
@extend .mt-base;
}
.relative {
position: relative;
}
.absolute {
position: absolute;
}
.overflow-hidden {
overflow: hidden;
}
.bg-white {
background-color: #ffffff;
}
.fw-bold {
font-weight: bold;
}
.text-center {
text-align: center;
}
.text-end {
text-align: end;
}
.w-100 {
width: 100%;
}
.TrainItem {
.OrderInfo {
font-size: 14px;
color: var(--color-info);
}
.Time {
font-size: 18px;
font-weight: bold;
}
.Station {
font-size: 14px;
color: var(--color-main);
}
.OptionItem {
display: flex;
flex-direction: column;
align-items: center;
padding-top: var(--space-base);
padding-bottom: var(--space-base);
color: var(--color-primary);
}
&.lg {
.Time {
font-size: 22px;
}
.Station {
font-size: 15px;
}
}
}
</style>

@ -0,0 +1,45 @@
# 新手常见问题
列举一些不好检索的,新手容易碰到的问题。
## 为什么我的 HBuilderx 的运行/打包按钮不可点?
HBuilderx 特性,需要在编辑器中打开任意项目下的文件才能运行/打包当前项目。
## 为什么我的项目运行不起来?
1. AppId没有获取
2. 环境没装好
3. 代码有问题
4. 依赖没装好
5. 依赖有问题
### AppID 有没有获取?
组长操作
[README.MD](./README.md#重要!写在开始!)
### 检查开发环境
安装[node](https://nodejs.cn/en/download/prebuilt-installer)
### 检查是不是代码运行之前不小心在键盘上敲了一下,导致代码错误?
拿到代码后第一时间上传git仓库这样每一次代码变更都有迹可循。
> 重新获取代码也可以
### 有没有安装依赖?
通过`git clone`的项目通常不会有第三方依赖文件夹,这需要你自己安装
前端项目有多种第三方依赖管理,常用的有`npm`、`yarn`,项目根目录下的`package.json`文件就是第三方依赖管理文件
运行安装命令如`npm install`后会在根目录下多出一个`node_modules`文件夹存放第三方依赖
### 确认第三方依赖安装的版本
可能你自动安装的依赖版本和开发者使用的依赖版本不一致,手动固定`package.json`中的依赖版本号后重新安装
> Mini-12306 已经固定了版本号

@ -0,0 +1,75 @@
# 重要!写在开始!
请每组开发人员统一使用的`AppID`,建议由小组组长生成后分享给小组成员
1. 组长获取代码,大概率是压缩包
2. 使用[HBuilder X](https://www.dcloud.io/hbuilderx.html)打开项目文件夹
3. 在根目录下找到并打开`manifest.json`文件,在`基础配置`中`重新获取AppID`
4. 上传代码到小组代码仓库(为什么?参考[新手常见问题](./QUESTIONS.md#检查是不是代码运行之前不小心在键盘上敲了一下,导致代码错误?)
5. 通知小组成员从代码仓库中重新获取项目代码
[DCloud appid 用途/作用/使用说明](https://ask.dcloud.net.cn/article/35907)
> 相同 appid 的项目打包的 apk 文件在安装时会相互覆盖,不同的则会共存(你也不想你组的 app 被其他组覆盖安装吧)
>
> 请保管好你组的 appid
# Mini-12306 APP
软件工程开发案例 - 前端app
以下内容为项目基本情况了解,实际上手开发可查看[开发指引](./README_DEVELOPMENT.md)
另附[新手常见问题](./QUESTIONS.md)
> 若要运行此项目,请先确保已安装依赖`npm install`
> 如果运行命令报错,请查看[新手常见问题](./QUESTIONS.md#检查开发环境)
## 关于前端开发
- 上手非常简单,开发工具完备
- 开发成果可视,正反馈很及时
> 前端开发在所有代码开发中的上手难度都是最低的
> 美观的页面可以给人留下非常好的第一印象
- 小团队中前端往往还会包含 UI 的职责
> 兼职 UI 的前端会很容易端陷入`细节调整`的陷阱:花大量时间死扣一个样式细节,导致整体进度被忽略。
---
## 开发语言
- [HTML](https://developer.mozilla.org/zh-CN/docs/Learn/HTML/Introduction_to_HTML/Getting_started)
- [JavaScript](https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/First_steps/A_first_splash)
- [CSS](https://developer.mozilla.org/zh-CN/docs/Learn/Getting_started_with_the_web/CSS_basics)
> 这是前端开发基础,掌握了基础才能看懂并使用框架来简化开发
>
> 就算你是完全的零基础,看完上面的链接基本就能上手了(网页侧边栏是完整教程,简单认识的话单看链接页面即可)
> 非常简单
## 开发框架
- [Vue2](https://v2.cn.vuejs.org/v2/guide/)
- [uni-app](https://uniapp.dcloud.net.cn/)
- [uview](https://www.uviewui.com/)
> 开发框架只需要做到`能看懂、会使用`就行
> 对于开发框架,重要的是熟练,也就是代码写的越多,越熟练
## 推荐 IDE
- [HBuilder X](https://www.dcloud.io/hbuilderx.html)
> uni-app 开发专用 IDE。
> 你也可以使用自己喜欢的编辑器,但相对的运行、打包等流程就需要自己去配置了。
## 代码规范参考
- [2021 阿里代码规范(前端篇)](https://developer.aliyun.com/article/850913)

@ -0,0 +1,89 @@
# Mini-12306 项目从零开发指引
项目使用 [uni-app](https://uniapp.dcloud.net.cn/) 前端应用框架,以类 [Vue](https://cn.vuejs.org/) 的开发方式进行 app 开发
以下是如何从零开始一个 uni-app 项目,建议按指引创建新项目先走一遍熟悉开发流程,再使用本项目中已有的代码进行开发
## 开发环境
1. 安装[node](https://nodejs.cn/en/download/prebuilt-installer)
2. 下载 uni-app 专用集成开发环境IDE[HBuilderX-高效极客技巧 (dcloud.io)](https://dcloud.io/hbuilderx.html)
3. 解压并打开`HBuilderX.exe`运行 IDE
4. 左上角`文件`→`新建`→`项目`→`uni-app`→`默认模板`
> 注意选择 vue2 版本。
> vue3 版本的 uni-app x 可以使用更高级的类型验证,但插件问题较多。
## 开发流程
### 项目结构简单说明
- `api` 前后端交互接口集中管理
- `components` 通用组件文件夹(先行了解 vue 组件概念)
- `node_modules` 第三方插件文件夹npm
- `pages` 所有能访问的页面
- `static` 静态资源(如不会修改的图标/图片)
- `store` 【进阶】Vuex跨页面数据管理
- `uni_modules` 这是 uni-app 中的 node_modulesuni-app 插件市场会导入到此文件夹)
- `utils` 常用方法、过滤器等工具
- `App.vue` 项目入口,主要是`onLauch`和全局样式
- `main.js` 项目入口,一般插件安装都会指引你在这个文件进行引入
- `manifest.json` 很重要的项目控制(打包)文件,但一般不会改到里面的东西
- `package.json` 与 node 中的同名文件对应,管理通过 npm 安装的第三方库
- `pages.json` 软件的路由配置(配置所有可以访问的页面)
- `uni.scss` 项目内置样式(注意里面只存放样式变量,不要在里面写实际样式)
### 项目运行(浏览器)
最方便开发肯定是一边改代码一边在设备中看到效果,
既然我们在 pc 上进行开发,那在浏览器中查看代码效果肯定是最方便的啦
1. HbuilderX 顶部菜单栏`运行`→`运行到浏览器`
2. 浏览器运行后打开`开发者模式F12`,切换`设备仿真`(一般是控制台左上角的第二个图标)
3. 修改项目代码,并保存,即可在浏览器中看到效果
![](README_DEVELOPMENT_files/1.jpg)
> 如果你是运行的本项目,记得使用`npm install`安装`package.json`中的依赖项
### 上手体验
参考`Hello uni-app`模板项目的源代码,在自己创建的空白项目中开发相关功能
> 对照着界面和代码一起看!(运行`Hello uni-app`项目)
1. 实现页面跳转(内置组件:导航 navigator`Hello uni-app/pages/component/navigator/navigator.vue`
2. 数据输入与提交(内置组件:表单组件)
3. 简单 js 功能(接口:界面-带数据的页面跳转、显示 loading 提示框,`Hello uni-app/pages/API/navigator/navigator.vue`
4. 与后端交互(接口:网络-发起一个请求,`Hello uni-app/pages/API/request/request.vue`
> 实际开发也是:
> → 搭建所有页面与路由配置(页面间跳转)
> → 在各页面中记录下要开发的功能
> → 实现功能逻辑(假数据)
> → 初步美化页面
> → 与后端对接(讨论修改)
> → 细节调整
## 项目打包 APP
HbuilderX 顶部菜单栏`发行:云端打包`(打包好后会在 HbuilderX 的控制台给出 apk 链接)
打包只需要勾选以下内容
- 使用公共测试证书
- 打正式包
- 快速安心打包
## 注意事项
- uview 要使用`npm`进行安装uni-app 插件市场安装的版本有问题
## 最后
以你们自己做的各种图来实现 Mini-12306
前端开发注意不要陷入页面样式细节的代码中去了,优先实现功能代码
> 建议参考`铁路12306`App 进行页面设计和功能拓展

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@ -0,0 +1,59 @@
/**
* 用户接口
*
* 因为 Mini-12306 不存在查看其他用户详情的交互
* 故不需要区分 auth user
*/
const apiAuth = {
/**
* 用户新增(注册)
*/
add: (params) => {
return uni.$u.http.post("/register", {
account: params.account, // 用户名
password: params.password, // 密码
name: params.name, // 姓名
idCardNo: params.idCardNo,
mobileNo: params.mobileNo,
mobileCode: params.mobileCode,
bankCard: params.bankCardNo,
});
},
/**
* 用户更新
*/
update: () => {},
/**
* 用户删除
*/
delete: () => {},
/**
* 用户详情
* 正常是通过用户id获取用户信息这里没有其他用户
* 故改为通过 auth-token 获取 auth 信息
*/
detail: () => {
return uni.$u.http.get("/auth");
},
/**
* 用户列表
*/
get: () => {},
/**
* 用户登录
*/
login: (params) => {
return uni.$u.http.post("/login", {
account: params.account, // 用户名
password: params.password, // 密码
});
},
};
export default apiAuth;

@ -0,0 +1,35 @@
const apiOrders = {
/**
* 订单创建
*/
add: (params) => {
return uni.$u.http.post("/orders", {
tickets: params.tickets.map((tkt) => ({
trainNo: tkt.trainNo, // 车次
from: tkt.from, // 上车
to: tkt.to, // 下车
date: uni.$u.dayjs(tkt.date).format("YYYY/MM/DD"), // 乘车日期python 后端要求这样转,按道理应该带上时区让后端处理)
seatClass: 0, // (暂无)座位类型,一等座/二等座(不同车次叫法不一样)
passengerId: tkt.passengerId, // 乘车人这种写法是不支持添加乘车人的因为你不知道他人id
})),
});
},
/**
* 订单支付
*/
payment: (params) => {
return uni.$u.http.post(`/bank/pay`, {
orderNo: params.orderNo,
});
},
/**
* 订单支付查询
*/
queryPayment: (params) => {
return uni.$u.http.post(`/orders/${params.orderNo}/query_payment`);
},
};
export default apiOrders;

@ -0,0 +1,12 @@
const apiOther = {
/**
* 发送手机验证码
*/
sendMobileCode: (params) => {
return uni.$u.http.post("/mobile/get_verify_code", {
mobileNo: params.mobileNo, // 手机号码
});
},
};
export default apiOther;

@ -0,0 +1,14 @@
/**
* 站点接口
*/
const apiStations = {
/**
* 站点列表
*/
get: (params) => {
return uni.$u.http.get("/stations");
},
};
export default apiStations;

@ -0,0 +1,18 @@
/**
* 车票接口
*/
const apiTickets = {
/**
* 车票列表
*/
get: (params) => {
return uni.$u.http.get("/tickets", {
params: {
state: 1, // 已出票(订单已支付)
},
});
},
};
export default apiTickets;

@ -0,0 +1,20 @@
/**
* 车次接口
*/
const apiTrains = {
/**
* 车次列表
*/
get: (params) => {
return uni.$u.http.get("/trains/query_train", {
params: {
from: params.from,
to: params.to,
date: params.date,
},
});
},
};
export default apiTrains;

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<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>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

@ -0,0 +1,39 @@
import Vue from "vue";
import App from "./App";
import store from "./store";
import "./uni.promisify.adaptor";
import uView from "uview-ui";
Vue.use(uView);
import {
dayjs,
timeFormat,
timeDiff,
hideIdCardNo,
hideMobileNo,
hideBankCardNo,
} from "./utils/filters";
Vue.filter("timeFormat", timeFormat); // 覆盖 uview 的设置
uni.$u.timeFormat = timeFormat;
uni.$u.date = undefined;
uni.$u.dayjs = dayjs;
Vue.filter("timeDiff", timeDiff);
Vue.filter("hideIdCardNo", hideIdCardNo);
Vue.filter("hideMobileNo", hideMobileNo);
Vue.filter("hideBankCardNo", hideBankCardNo);
Vue.config.productionTip = false;
App.mpType = "app";
const app = new Vue({
...App,
store,
});
// 引入请求封装将app参数传递到配置中
require("./utils/request.js")(app);
app.$mount();

@ -0,0 +1,118 @@
{
"name": "Mini-12306",
"appid": "",
"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_MOBILE_NO_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": {
"dSYMs": false
},
/* SDK */
"sdkConfigs": {
"ad": {}
},
"icons": {
"android": {
"hdpi": "static/app-icon/72x72.png",
"xhdpi": "static/app-icon/96x96.png",
"xxhdpi": "static/app-icon/144x144.png",
"xxxhdpi": "static/app-icon/192x192.png"
},
"ios": {
"appstore": "",
"ipad": {
"app": "",
"app@2x": "",
"notification": "",
"notification@2x": "",
"proapp@2x": "",
"settings": "",
"settings@2x": "",
"spotlight": "",
"spotlight@2x": ""
},
"iphone": {
"app@2x": "",
"app@3x": "",
"notification@2x": "",
"notification@3x": "",
"settings@2x": "",
"settings@3x": "",
"spotlight@2x": "",
"spotlight@3x": ""
}
}
},
"splashscreen": {
"androidStyle": "default",
"android": {
"hdpi": "static/splashscreen/hdpi/_20240628103445.9.png",
"mdpi": "static/splashscreen/mdpi/_20240628103445.9.png",
"xhdpi": "static/splashscreen/xhdpi/_20240628103445.9.png",
"xxhdpi": "static/splashscreen/xxhdpi/_20240628103445.9.png",
"xxxhdpi": "static/splashscreen/xxxhdpi/_20240628103445.9.png"
}
}
}
},
/* */
"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": "2"
}

@ -0,0 +1,26 @@
{
"name": "Mini-12306",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"dayjs": "^1.11.11",
"uview-ui": "^2.0.36"
}
},
"node_modules/dayjs": {
"version": "1.11.11",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.11.tgz",
"integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg=="
},
"node_modules/uview-ui": {
"version": "2.0.36",
"resolved": "https://registry.npmmirror.com/uview-ui/-/uview-ui-2.0.36.tgz",
"integrity": "sha512-ASSZT6M8w3GTO1eFPbsgEFV0U5UujK+8pTNr+MSUbRNcRMC1u63DDTLJVeArV91kWM0bfAexK3SK9pnTqF9TtA==",
"engines": {
"HBuilderX": "^3.1.0"
}
}
}
}

@ -0,0 +1,6 @@
{
"dependencies": {
"dayjs": "1.11.11",
"uview-ui": "2.0.36"
}
}

@ -0,0 +1,101 @@
{
"easycom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
},
"pages": [
//pageshttps://uniapp.dcloud.io/collocation/pages
{
"path": "pages/HomeView/HomeView",
"style": {
"navigationBarTitleText": "首页",
"navigationBarBackgroundColor": "#f5f5f5",
"app-plus": {
"titleNView": {
"type": "transparent",
"titleColor": "#303133"
}
}
}
},
{
"path": "pages/QueryTrainView/QueryTrainView",
"style": {
"navigationBarTitleText": "查询车次"
}
},
{
"path": "pages/BuyTicketView/BuyTicketView",
"style": {
"navigationBarTitleText": "确认购票"
}
},
{
"path": "pages/MyTicketsView/MyTicketsView",
"style": {
"navigationBarTitleText": "本人车票"
}
},
{
"path": "pages/LoginView/LoginView",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/RegistrationView/RegistrationView",
"style": {
"navigationBarTitleText": "注册"
}
},
{
"path": "pages/UserView/UserView",
"style": {
"navigationBarTitleText": "个人主页",
"navigationStyle": "custom"
}
},
{
"path": "pages/SelectStationView/SelectStationView",
"style": {
"navigationBarTitleText": "车站选择"
}
}
],
"tabBar": {
"backgroundColor": "#ffffff",
"color": "#000000", //
"selectedColor": "#3b86ff",
"list": [
//
{
"pagePath": "pages/HomeView/HomeView",
"text": "首页",
"iconPath": "static/icon_home_normal.png",
"selectedIconPath": "static/icon_home_selected.png"
},
{
"pagePath": "pages/MyTicketsView/MyTicketsView",
"text": "车票",
"iconPath": "static/icon_order_normal.png",
"selectedIconPath": "static/icon_order_selected.png"
},
{
"pagePath": "pages/UserView/UserView",
"text": "我的",
"iconPath": "static/icon_mine_normal.png",
"selectedIconPath": "static/icon_mine_selected.png"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#238cfc",
"backgroundColor": "#F8F8F8",
"app-plus": {
"bounce": "none"
}
},
"uniIdRouter": {}
}

@ -0,0 +1,229 @@
<template>
<view class="buy-ticket-view p-page">
<view class="page-box TrainItem lg p-base">
<view class="text-primary text-center"
>{{ $u.dayjs(date).format("MM月DD日") }}&ensp;{{
$u.dayjs(date).isToday() ? "今天" : $u.timeFormat(date, "[周]dd")
}}</view
>
<u-row :gutter="12" class="mt-base py-llg">
<u-col :span="4">
<view class="Time">{{ fromTime | timeFormat("HH:mm") }}</view>
<view class="Station">{{ from }}</view>
</u-col>
<u-col :span="4">
<view class="text-center" :style="{ fontSize: '14px' }">
<view>{{ trainNo }}</view>
<u-line></u-line>
<view>{{ fromTime | timeDiff(toTime) }}</view>
</view>
</u-col>
<u-col :span="4">
<view class="Time text-end">{{ toTime | timeFormat("HH:mm") }}</view>
<view class="Station text-end">{{ to }}</view>
</u-col>
</u-row>
</view>
<view class="mt-base p-base bg-white rounded-sm">
<view>
<u-text text="乘车人" :size="18" :bold="true"></u-text>
</view>
<view class="my-base">
<u-line></u-line>
</view>
<view>
<u-text :text="$store.state.auth.name"></u-text>
</view>
<view class="mt-sm">
<u-text
type="info"
:size="12"
:text="$store.state.auth.idCardNo | hideIdCardNo"
></u-text>
</view>
</view>
<view class="mt-base" :style="{ fontSize: '13px' }">
<text class="text-info">确定支付表示已阅读并同意</text
><text class="text-primary"
>国铁集团铁路旅客运输规程服务条款</text
>
</view>
<view class="mt-llg">
<u-button
type="primary"
:custom-style="{ borderRadius: `var(--rounded-sm)` }"
@tap="handlePayment"
>确定支付</u-button
>
</view>
<view
class="relative rounded-sm"
:style="{
'--title-move-y': '-13px',
marginTop: 'calc(var(--space-llg) - var(--title-move-y))',
border: '1px dashed #ceccca',
}"
>
<view
class="absolute"
:style="{
backgroundColor: 'var(--color-background)',
marginLeft: '-2px',
paddingLeft: '2px',
height: '26px',
top: 'var(--title-move-y)',
left: '0',
}"
>
<view class="u-flex-y-center" :style="{ fontSize: '15px' }">
<u-icon name="info-circle-fill" color="var(--color-primary)"></u-icon>
<text class="ms-sm fw-bold">温馨提示</text>
</view>
</view>
<view class="p-sm pt-llg text-info" :style="{ fontSize: '13px' }">
<!--
<view
>1.一天内3次申请车票成功后取消订单包含无座车票或不符合选铺需求车票时取消5次计为取消1次当日将不能在Mini-12306继续购票</view
>
-->
<view
>如因运力原因或其他不可控因素导致列车调度调整时当前车型可能会发生变动</view
>
</view>
</view>
</view>
</template>
<script>
import { mapState } from "vuex";
import apiOrders from "@/api/orders.js";
import apiTickets from "@/api/tickets";
export default {
data() {
return {
trainNo: "",
from: "",
fromTime: "",
to: "",
toTime: "",
date: "",
passengers: [],
};
},
computed: {
...mapState({ auth: "auth" }),
},
onLoad(query) {
if (!this.auth.id) {
uni.navigateTo({ url: "/pages/LoginView/LoginView" });
return;
}
/**
* 默认添加当前登录用户作为乘车人
*/
this.passengers.push({
id: this.auth.id,
name: this.auth.name,
idCardNo: this.auth.idCardNo,
});
const { train_no, from, from_time, to, to_time, date } = query;
this.trainNo = decodeURIComponent(train_no);
this.from = decodeURIComponent(from);
this.fromTime = decodeURIComponent(from_time);
this.to = decodeURIComponent(to);
this.toTime = decodeURIComponent(to_time);
this.date = this.$u.dayjs(decodeURIComponent(date)).format();
},
methods: {
handlePayment() {
/**
* 这里将订单确认与支付合并到了一个页面
* 实际上应该确认订单后跳转到支付页面
*
* 订单层其实是预留了实现一个订单多张车票乘车人的情况
*/
uni.showLoading({ title: "正在确认订单", mask: true });
apiOrders
.add({
tickets: this.passengers.map((pax) => ({
trainNo: this.trainNo,
from: this.from,
to: this.to,
date: this.date,
passengerId: pax.id,
})),
})
.then((apiOrdersAddRes) => {
uni.hideLoading();
/**
* 支付订单
*/
uni.showLoading({ title: "正在支付", mask: true });
apiOrders
.payment({
orderNo: apiOrdersAddRes.data.orderNo,
})
.then(() => {
uni.hideLoading();
const wait = 1000;
uni.showToast({
title: "支付成功",
mask: true,
duration: wait,
});
setTimeout(() => {
/**
* 查询支付结果
*/
uni.showLoading({ title: "查询订单状态", mask: true });
apiOrders
.queryPayment({
orderNo: apiOrdersAddRes.data.orderNo,
})
.then(() => {
uni.hideLoading();
const wait = 1000;
uni.showToast({
title: "订单已支付",
mask: true,
duration: wait,
});
setTimeout(() => {
uni.switchTab({
url: "/pages/MyTicketsView/MyTicketsView",
});
}, wait);
})
.catch(() => uni.hideLoading());
}, wait);
})
.catch(() => uni.hideLoading());
})
.catch(() => uni.hideLoading());
},
},
};
</script>
<style scoped>
.buy-ticket-view {
background: linear-gradient(
var(--color-app-blue) 0 40px,
80px,
var(--color-background) 120px
);
}
</style>

@ -0,0 +1,233 @@
<template>
<view class="p-page">
<view
class="page-box-t-overflow page-box-x-overflow"
:style="{ marginBottom: '-12px' }"
>
<u-swiper
:list="[
'https://www.12306.cn/index/images/pic/banner11.jpg',
'https://www.12306.cn/index/images/pic/banner12.jpg',
]"
:radius="0"
:height="200"
:circular="true"
></u-swiper>
</view>
<view class="page-box py-base px-lg relative">
<view>
<u-text type="primary" :size="18" :bold="true" text="火车票"></u-text>
</view>
<view class="mt-base py-base">
<!-- flex 布局总是会因为左右字符长度不同导致中心偏移因此采用 row 布局 -->
<u-row>
<u-col :span="5">
<view @tap="handleSelectStation('from')">
<u-text
:type="from ? undefined : 'info'"
:size="20"
:bold="true"
align="left"
:text="from || '请选择'"
></u-text>
</view>
</u-col>
<u-col :span="2">
<view class="u-flex-x-center">
<u-icon name="arrow-right-double" :size="22"></u-icon>
</view>
</u-col>
<u-col :span="5">
<view @tap="handleSelectStation('to')">
<u-text
:type="to ? undefined : 'info'"
:size="20"
:bold="true"
align="right"
:text="to || '请选择'"
></u-text>
</view>
</u-col>
</u-row>
</view>
<u-line></u-line>
<!-- uLine 颜色比较浅 -->
<view class="py-base">
<view class="u-flex-y-center" @tap="showStationDate = true">
<!-- 直接在 uText 上写的样式可能会在页面更新时被莫名的 bug 覆盖因此要养成间距类样写在 view 标签上的好习惯-->
<view :style="{ flexGrow: 0, whiteSpace: 'nowrap' }">
<u-text :size="20" :text="$u.timeFormat(date, 'M月D日')"></u-text>
</view>
<view :style="{ flexGrow: 1, marginLeft: '0.6em' }">
<u-text
type="info"
:text="
$u.dayjs(date).isToday()
? '今天'
: $u.timeFormat(date, '[周]dd')
"
></u-text>
</view>
</view>
</view>
<u-line></u-line>
<u-calendar
:show="showStationDate"
:min-date="$u.dayjs().format()"
:max-date="$u.dayjs().add(14, 'day').format()"
@confirm="handledateConfirm"
@close="showStationDate = false"
></u-calendar>
<view class="mt-base">
<u-button
type="primary"
:disabled="!from || !to"
@tap="handleToQueryTrain"
>查询车票</u-button
>
</view>
</view>
<!--
<view class="page-box">
<u-grid :border="false" :col="5">
<u-grid-item v-for="item in HOME_MENU" :key="item.label">
<view class="grid-item">
<u-image :src="item.image" width="30px" height="30px"></u-image>
<view class="grid-item-text">{{ item.label }}</view>
</view>
</u-grid-item>
</u-grid>
</view>
-->
<!--
<view class="page-box p-base u-flex-y-center">
<u-image
src="@/static/ic_hot_news.png"
:width="40"
:height="40"
mode="scaleToFill"
class="u-flex-shrink-0"
></u-image>
<view class="ms-lg u-flex-fill">
<u-text
text="中铁快运创新退出跨域当日达"
:size="14"
:bold="true"
></u-text>
<u-text
text='"高铁急送"城市群内4小时城市群间8小时送达'
:size="14"
></u-text>
</view>
<view class="u-flex-shrink-0 u-flex-y-center">
<u-icon name="arrow-right"></u-icon>
</view>
</view>
-->
<!--
<view class="page-box overflow-hidden">
<u-image
src="https://www.12306.cn/index/images/abanner01.jpg"
mode="widthFix"
height="auto"
width="100%"
></u-image>
</view>
<view class="page-box overflow-hidden">
<u-image
src="https://www.12306.cn/index/images/abanner05.jpg"
mode="widthFix"
height="auto"
width="100%"
></u-image>
</view>
-->
</view>
</template>
<script>
import { mapState } from "vuex";
import uNavbarOpacity from "@/utils/mixins/u-navbar-opacity";
const HOME_MENU = [
{ label: "车站大屏", image: require("@/static/home_daping.png") },
{ label: "计次·定期票", image: require("@/static/home_jicipiao.png") },
{ label: "铁路e卡通", image: require("@/static/home_ekatong.png") },
{ label: "时刻表", image: require("@/static/home_liechezhuangtai.png") },
{ label: "温馨服务", image: require("@/static/home_wenxinfuwu.png") },
{ label: "空铁联运", image: require("@/static/home_kongtie.png") },
{ label: "敬老版", image: require("@/static/home_aixin.png") },
{ label: "酒店住宿", image: require("@/static/home_jiudian.png") },
{ label: "约车", image: require("@/static/home_yueche.png") },
{ label: "门票·旅游", image: require("@/static/home_trip.png") },
{ label: "餐饮·特产", image: require("@/static/home_canyin.png") },
{ label: "汽车票", image: require("@/static/home_qichepiao.png") },
{ label: "铁路商城", image: require("@/static/home_shangcheng.png") },
{
label: "中铁畅行卡",
image: require("@/static/home_zhongtiechangxingka.png"),
},
{ label: "出行保险", image: require("@/static/home_chengyixian.png") },
];
export default {
mixins: [uNavbarOpacity],
data() {
return {
from: "",
to: "",
showStationDate: false,
date: new Date(),
HOME_MENU,
searchInput: "",
};
},
computed: {
...mapState({ stations: "stations" }),
},
methods: {
handleSelectStation(key) {
uni.navigateTo({
url: `/pages/SelectStationView/SelectStationView?key=${key}`,
});
},
handledateConfirm(e) {
this.date = this.$u.dayjs(e[0]).startOf("date").format();
this.showStationDate = false;
},
handleToQueryTrain() {
uni.navigateTo({
url: `/pages/QueryTrainView/QueryTrainView?from=${encodeURIComponent(
this.from
)}&to=${encodeURIComponent(this.to)}&date=${encodeURIComponent(
this.date
)}`,
});
},
},
};
</script>
<style scoped>
.grid-item {
display: flex;
flex-direction: column;
align-items: center;
padding-top: var(--space-base);
padding-bottom: var(--space-base);
}
.grid-item-text {
margin-top: var(--space-ssm);
font-size: 12px;
white-space: nowrap;
}
</style>

@ -0,0 +1,136 @@
<template>
<view class="p-page">
<u-navbar
:placeholder="true"
bg-color="transparent"
:auto-back="true"
></u-navbar>
<view class="mt-llg p-base u-flex u-flex-column u-flex-items-center">
<u-image
src="@/static/logo.png"
:width="60"
:height="60"
:radius="10"
></u-image>
<view class="mt-sm">
<u-text text="欢迎登录" :size="20"></u-text>
</view>
</view>
<view class="p-base">
<u-form
label-position="top"
labelWidth="auto"
:model="formData"
:rules="formRules"
ref="formRef"
>
<u-form-item prop="account">
<u-input
v-model="formData.account"
:custom-style="{ backgroundColor: 'white' }"
@focus="$refs.formRef.clearValidate('account')"
>
<view slot="prefix" class="u-flex-y-center">
<view class="ms-base me-sm">用户</view>
<u-line
direction="col"
length="26px"
:style="{ margin: '0 var(--space-base)' }"
></u-line>
</view>
</u-input>
</u-form-item>
<u-form-item prop="password">
<u-input
v-model="formData.password"
type="password"
:custom-style="{ backgroundColor: 'white' }"
@focus="$refs.formRef.clearValidate('password')"
>
<view slot="prefix" class="u-flex-y-center">
<view class="ms-base me-sm">密码</view>
<u-line
direction="col"
length="26px"
:style="{ margin: '0 var(--space-base)' }"
></u-line> </view
></u-input>
</u-form-item>
</u-form>
<view class="mt-llg">
<u-button type="primary" @tap="handleLogin"></u-button>
</view>
<view class="mt-base">
<u-button @tap="handleToRegister"></u-button>
</view>
<view class="mt-llg text-center text-primary">
<text :style="{ fontSize: '14px' }">服务条款隐私权政策</text>
</view>
</view>
</view>
</template>
<script>
import { mapActions } from "vuex";
import { validateRequired } from "@/utils/form-validators";
import apiAuth from "@/api/auth";
export default {
data() {
return {
formData: {
account: "",
password: "",
},
formRules: {
// trigger
account: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, { label: "用户名" }),
},
password: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, { label: "密码" }),
},
},
loading: false,
};
},
methods: {
...mapActions({ updateAuth: "updateAuth" }),
handleLogin() {
this.$refs.formRef
.validate()
.then(() => {
uni.showLoading({ title: "正在登录", mask: true });
apiAuth
.login({
account: this.formData.account,
password: this.formData.password,
})
.then((res) => {
uni.hideLoading();
this.updateAuth(res.data);
uni.switchTab({
url: "/pages/HomeView/HomeView",
});
})
.catch(() => uni.hideLoading());
})
.catch();
},
handleToRegister() {
uni.navigateTo({
url: "/pages/RegistrationView/RegistrationView",
});
},
},
};
</script>
<style></style>

@ -0,0 +1,155 @@
<template>
<view class="p-page">
<view
v-if="!auth.id || (!loading && tickets.length === 0)"
class="u-flex-xy-center"
:style="{ height: '70vh' }"
>
<view v-if="!auth.id">
<view>
<text>请先登录</text>
</view>
<view class="mt-base">
<u-button type="primary" size="small" @tap="handleToLogin"
>去登录</u-button
>
</view>
</view>
<u-empty
v-else-if="!loading && tickets.length === 0"
mode="order"
icon-color="#9acafc"
text="无购票记录"
text-size="16px"
></u-empty>
</view>
<view
v-for="ticket in tickets"
:key="ticket.orderNO"
class="page-box TrainItem lg"
>
<view class="p-base u-flex u-flex-between OrderInfo">
<text>订单号{{ ticket.orderNO }}</text>
<text>车票仅当日档次有效</text>
</view>
<u-line></u-line>
<view class="px-base py-llg">
<view>
<u-row :gutter="12">
<u-col :span="4">
<view class="Time">{{
ticket.fromTime | timeFormat("HH:mm")
}}</view>
<view class="Station">{{ ticket.from }}</view>
</u-col>
<u-col :span="4">
<view class="text-center">
<view class="my-ssm">{{ ticket.trainNo }}</view>
<u-line></u-line>
<view
class="my-ssm text-center text-info"
:style="{ fontSize: '15px' }"
>{{ $u.dayjs(ticket.date).format("MM月DD日") }}&ensp;{{
$u.dayjs(ticket.date).isToday()
? "今天"
: $u.timeFormat(ticket.date, "[周]dd")
}}</view
>
</view>
</u-col>
<u-col :span="4">
<view class="Time text-end">{{
ticket.toTime | timeFormat("HH:mm")
}}</view>
<view class="Station text-end">{{ ticket.to }}</view>
</u-col>
</u-row>
</view>
<view class="mt-base u-flex-y-center u-flex-between OrderInfo">
<text>已支付</text>
<text class="text-warning" :style="{ fontSize: '15px' }"
>{{ ticket.price }}</text
>
</view>
</view>
<!--
<u-line></u-line>
<view class="u-flex-y-center">
<view class="u-flex-grow OptionItem">改签</view>
<u-line direction="col" length="26px"></u-line>
<view class="u-flex-grow OptionItem">退票</view>
<u-line direction="col" length="26px"></u-line>
<view class="u-flex-grow OptionItem">变更到站</view>
</view>
-->
</view>
</view>
</template>
<script>
import { mapState } from "vuex";
import apiTickets from "@/api/tickets";
import { timeAscCompareFn } from "@/utils/functions.js";
export default {
data() {
return {
tickets: [],
loading: false,
};
},
computed: {
...mapState({ auth: "auth" }),
},
/* 注意这里是 onShow 而不是 onLoad详情查看 uniapp 生命周期 */
onShow() {
if (this.auth.id) {
this.loadMyTickets();
}
},
methods: {
handleToLogin() {
uni.navigateTo({ url: "/pages/LoginView/LoginView" });
},
loadMyTickets() {
this.loading = true;
uni.showLoading({ title: "正在加载" });
apiTickets
.get()
.then((res) => {
//
this.tickets = res.data
.sort(
(a, b) =>
timeAscCompareFn(a.fromTime, b.fromTime) ||
timeAscCompareFn(a.toTime, b.toTime) ||
a.id - b.id
)
.filter(
(tkt) =>
!uni.$u
.dayjs(tkt.fromTime)
.isBefore(uni.$u.dayjs().startOf("date"))
);
})
.catch(() => {
this.tickets = [];
})
.then(() => {
this.loading = false;
uni.hideLoading();
});
},
},
};
</script>
<style></style>

@ -0,0 +1,234 @@
<template>
<view>
<u-sticky>
<scroll-view scroll-x="true" class="DatePicker">
<view
v-for="allowableDate in allowableDates"
:key="allowableDate"
:class="[
'DatePickerItem',
{ 'is-active': $u.dayjs(allowableDate).isSame(date, 'day') },
]"
@tap="handleChangeDate(allowableDate)"
>
<view :style="{ fontSize: '14px', textAlign: 'center' }">{{
$u.dayjs(allowableDate).isToday()
? "今天"
: $u.timeFormat(allowableDate, "[周]dd")
}}</view>
<view>{{ allowableDate | timeFormat("MM.DD") }}</view>
</view>
</scroll-view>
</u-sticky>
<view class="p-page">
<view
v-if="!loading && trains.length === 0"
:style="{ textIndent: '2em' }"
>
<text class="text-content"
>很抱歉按您的查询条件当前未找到从<text class="mx-ssm fw-bold">{{
from
}}</text
><text class="mx-ssm fw-bold">{{ to }}</text
>的列车</text
>
</view>
<view
v-for="train in trains"
:key="train.trainNo"
class="page-box TrainItem p-base"
>
<u-row :gutter="12">
<u-col :span="3">
<view class="Time">{{
train.query.fromTime | timeFormat("HH:mm")
}}</view>
<view class="Station">{{ train.query.from }}</view>
</u-col>
<u-col :span="3">
<view class="text-center" :style="{ fontSize: '14px' }">
<view>{{ train.trainNo }}</view>
<u-line></u-line>
<view>{{
train.query.fromTime | timeDiff(train.query.toTime)
}}</view>
</view>
</u-col>
<u-col :span="3">
<view class="Time text-end">{{
train.query.toTime | timeFormat("HH:mm")
}}</view>
<view class="Station text-end">{{ train.query.to }}</view>
</u-col>
<u-col :span="3">
<view class="u-flex-center ps-lg">
<u-button
type="primary"
size="mini"
@tap="handleToByTicket(train)"
>预定</u-button
>
</view>
</u-col>
</u-row>
</view>
</view>
</view>
</template>
<script>
import apiTrains from "@/api/trains.js";
export default {
data() {
return {
allowableDates: [], //
from: "", //
to: "", //
date: "", //
trains: [], //
loading: false, //
};
},
computed: {
queryTrainParams() {
return {
from: this.from,
to: this.to,
date: this.date,
};
},
},
watch: {
queryTrainParams(val) {
this.handleQueryTrain(val);
},
},
onLoad(query) {
const { from, to, date } = query;
this.from = decodeURIComponent(from);
this.to = decodeURIComponent(to);
this.date = this.$u.dayjs(decodeURIComponent(date)).format();
this.initAllowableDates();
},
onReady() {
// `onReady`
uni.setNavigationBarTitle({ title: `${this.from} > ${this.to}` });
uni.setNavigationBarColor({
backgroundColor: "#3c9cff", // --color-primary
frontColor: "#ffffff",
});
},
methods: {
initAllowableDates() {
// 15
for (let i = 0; i < 15; i += 1) {
this.allowableDates.push(this.$u.dayjs().add(i, "day").format());
}
},
handleChangeDate(date) {
this.date = date;
},
handleQueryTrain(params) {
//
if (!params || !params.from || !params.to || !params.date) {
uni.switchTab({ url: "/pages/HomeView/HomeView" });
}
uni.showLoading({ title: "正在加载", mask: true });
this.loading = true;
apiTrains
.get({
from: params.from,
to: params.to,
date: params.date,
})
.then((res) => {
this.trains = res.data.map((trn) => ({
...trn,
query: this.formatTrainToQuery(trn, params.from, params.to),
}));
})
.catch(() => {})
.then(() => {
uni.hideLoading();
this.loading = false;
});
},
handleToByTicket(train) {
if (!this.$store.state.auth.id) {
uni.navigateTo({ url: "/pages/LoginView/LoginView" });
return;
}
uni.navigateTo({
url: `/pages/BuyTicketView/BuyTicketView?train_no=${encodeURIComponent(
train.trainNo
)}&from=${encodeURIComponent(
train.query.from
)}&from_time=${encodeURIComponent(
train.query.fromTime
)}&to=${encodeURIComponent(
train.query.to
)}&to_time=${encodeURIComponent(
train.query.toTime
)}&date=${encodeURIComponent(this.date)}`,
});
},
/**
* 以三个参数获取车次查询需要的显示对象
* @param {Object} train
* @param {String} from 上车站from_station)
* @param {String} to 下车站to_station
*/
formatTrainToQuery(train, from, to) {
const stations = train.stations;
const fromIndex = stations.findIndex((stn) => stn.name === from);
const toIndex = stations.findIndex((stn) => stn.name === to);
let price = 0;
for (let i = fromIndex; i <= toIndex; i += 1) {
price += stations[i].price;
}
return {
from: from, //
fromTime: stations[fromIndex].depTime, //
to: to, //
toTime: stations[toIndex].arrTime, //
};
},
},
};
</script>
<style scoped lang="scss">
$date-picker-bg: $u-primary;
$date-picker-color: #ffffff;
.DatePicker {
white-space: nowrap;
padding: var(--space-sm) var(--space-base);
background-color: $date-picker-bg;
color: $date-picker-color;
}
.DatePickerItem + .DatePickerItem {
margin-left: var(--space-sm);
}
.DatePickerItem {
display: inline-block;
padding: var(--space-ssm);
border-radius: calc(var(--space-ssm) * 1.5);
&.is-active {
background-color: $date-picker-color;
color: $date-picker-bg;
}
}
</style>

@ -0,0 +1,386 @@
<template>
<view class="py-base">
<u-form
:model="formData"
:rules="formRules"
errorType="border-bottom"
ref="formRef"
>
<u-form-item>
<view class="px-base pt-sm">
<u--text type="info" text="基本信息"></u--text>
</view>
</u-form-item>
<u-form-item
prop="account"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>&ensp;&ensp;</view>
<u-input
v-model="formData.account"
placeholder='字母、数字或"_"6~30位'
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('account')"
></u-input>
</view>
</u-form-item>
<u-form-item
prop="password"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>&emsp;&emsp;</view>
<u-input
v-model="formData.password"
placeholder='字母、数字或"_"组合6~30位'
type="password"
border="none"
input-align="right"
@focus="$refs.formRef.clearValidate('password')"
></u-input>
</view>
</u-form-item>
<u-form-item
prop="passwordConfirm"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>确认密码</view>
<u-input
v-model="formData.passwordConfirm"
placeholder="请再次输入密码"
type="password"
border="none"
input-align="right"
@focus="$refs.formRef.clearValidate('passwordConfirm')"
></u-input>
</view>
</u-form-item>
<u-form-item>
<view class="px-base pt-sm">
<u--text type="info" text="证件信息"></u--text>
</view>
</u-form-item>
<u-form-item
prop="name"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>&emsp;&emsp;</view>
<u-input
v-model="formData.name"
placeholder="请输入真实姓名,以便购票"
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('name')"
></u-input>
</view>
</u-form-item>
<u-form-item
prop="idCardNo"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>身份证号</view>
<u-input
v-model="formData.idCardNo"
placeholder="请输入身份证号码"
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('idCardNo')"
></u-input>
</view>
</u-form-item>
<u-form-item>
<view class="px-base pt-sm">
<u--text type="info" text="联系方式"></u--text>
</view>
</u-form-item>
<u-form-item
prop="mobileNo"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>手机号码</view>
<u-input
v-model="formData.mobileNo"
placeholder="请输入手机号码"
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('mobileNo')"
>
<template slot="suffix"
><u-code ref="codeRef" @change="handleCodeChange"></u-code
><u-button type="success" size="mini" @tap="handleSendCode">{{
codeTips
}}</u-button></template
>
</u-input>
</view>
<u-modal
:show="mobileCodeModalVisible"
:async-close="true"
title="验证码"
confirm-text="复制验证码"
@confirm="handleMobileCodeModalConfirm"
>
<view class="text-center">
<text :style="{ fontSize: '24px' }">{{
mobileCodeModalValue
}}</text>
</view>
</u-modal>
</u-form-item>
<u-form-item
prop="mobileCode"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>&ensp;&ensp;</view>
<u-input
v-model="formData.mobileCode"
placeholder="请输入手机验证码"
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('mobileCode')"
></u-input>
</view>
</u-form-item>
<u-form-item>
<view class="px-base pt-sm">
<u--text type="info" text="支付信息"></u--text>
</view>
</u-form-item>
<u-form-item
prop="bankCardNo"
:border-bottom="true"
:custom-style="{ backgroundColor: '#ffffff' }"
>
<view class="w-100 px-base bg-white u-flex-y-center">
<view>银行卡号</view>
<u-input
v-model="formData.bankCardNo"
placeholder="请输入银行卡号"
border="none"
input-align="right"
class="u-flex-grow"
@focus="$refs.formRef.clearValidate('bankCardNo')"
></u-input>
</view>
</u-form-item>
<u-form-item><!-- 当间距用 --></u-form-item>
<view class="px-base mb-lg u-flex-y-center">
<u-checkbox-group v-model="checkboxValues">
<u-checkbox name="agreeTerms" label="同意"></u-checkbox>
</u-checkbox-group>
<text class="text-primary" :style="{ fontSize: '15px' }"
>服务条款隐私权政策</text
>
</view>
<u-form-item>
<u-button type="primary" @tap="handleRegister"></u-button>
</u-form-item>
</u-form>
<u-modal
:show="errorModalVisible"
title="温馨提示"
:content="errorModalValue"
@confirm="errorModalVisible = false"
></u-modal>
</view>
</template>
<script>
import {
validateRequired,
validateAccount,
validatePassword,
validatePasswordConfirm,
} from "@/utils/form-validators";
import apiAuth from "@/api/auth.js";
import apiOther from "@/api/other.js";
export default {
data() {
return {
formData: {
account: "",
password: "",
passwordConfirm: "",
name: "",
idCardNo: "",
mobileNo: "",
mobileCode: "",
bankCardNo: "",
},
formRules: {
// 使 trigger
account: { validator: validateAccount, trigger: "blur" },
password: { validator: validatePassword, trigger: "blur" },
passwordConfirm: {
validator: (rule, value, callback) =>
validatePasswordConfirm(
rule,
value,
callback,
this.formData.password
),
trigger: "blur",
},
name: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, {
label: "姓名",
trim: true,
}),
trigger: "blur",
},
idCardNo: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, {
label: "身份证号",
trim: true,
}),
trigger: "blur",
},
mobileNo: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, {
label: "手机号码",
trim: true,
}),
trigger: "blur",
},
mobileCode: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, {
label: "验证码",
trim: true,
}),
trigger: "blur",
},
bankCardNo: {
validator: (rule, value, callback) =>
validateRequired(rule, value, callback, {
label: "银行卡号",
trim: true,
}),
trigger: "blur",
},
},
codeTips: "",
mobileCodeModalVisible: false,
mobileCodeModalValue: "",
checkboxValues: [],
errorModalVisible: false,
errorModalValue: "",
};
},
methods: {
handleRegister() {
this.$refs.formRef
.validate()
.then(() => {
if (!this.checkboxValues.includes("agreeTerms")) {
this.openErrorModal("同意《服务条款》才能注册");
return;
}
uni.showLoading({ title: "正在注册", mask: true });
apiAuth
.add(this.formData)
.then(() => {
uni.hideLoading();
const wait = 1000;
uni.showToast({
title: "注册成功,请使用新账号登录",
mask: true,
duration: wait,
});
setTimeout(() => {
uni.redirectTo({ url: "/pages/LoginView/LoginView" });
}, wait);
})
.catch(() => uni.hideLoading());
})
.catch((errors) => {
this.openErrorModal(errors[0].message);
});
},
handleSendCode() {
if (this.$refs.codeRef.canGetCode) {
this.$refs.formRef.validateField("mobileNo", (errors) => {
if (!errors.length) {
uni.showLoading({ title: "正在发送验证码", mask: true });
apiOther
.sendMobileCode({
mobileNo: this.formData.mobileNo,
})
.then((res) => {
uni.hideLoading();
uni.$u.toast("验证码发送成功");
this.$refs.codeRef.start();
// code
this.mobileCodeModalValue = "";
setTimeout(() => {
uni.hideToast();
this.mobileCodeModalValue = res.data.code;
this.mobileCodeModalVisible = true;
}, 1000);
})
.catch(() => uni.hideLoading());
}
});
}
},
handleCodeChange(text) {
this.codeTips = text;
},
handleMobileCodeModalConfirm() {
uni.setClipboardData({
data: String(this.mobileCodeModalValue),
success: () => {
this.mobileCodeModalVisible = false;
},
fail: (e) => {
uni.showToast({
icon: "error",
title: "复制失败",
});
},
});
},
openErrorModal(content) {
this.errorModalValue = content;
this.errorModalVisible = true;
},
},
};
</script>
<style></style>

@ -0,0 +1,59 @@
<template>
<u-index-list :index-list="stationList.indexList">
<template v-for="(item, index) in stationList.itemArr">
<u-index-item>
<u-index-anchor :text="stationList.indexList[index]"></u-index-anchor>
<view
v-for="(cell, i) in item"
@tap="handleSelectedStation(cell)"
class="py-base mx-base"
:class="[{ 'u-border-bottom': i !== item.length - 1 }]"
>{{ cell }}</view
>
</u-index-item>
</template>
</u-index-list>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
return {
key: "",
};
},
computed: {
...mapState({ stations: "stations" }),
stationList() {
// updateStations stations
const indexList = [];
const itemArr = [];
for (const station of this.stations) {
const currChar = indexList.slice(-1)[0];
const char = station.pinyin[0];
if (currChar === char) {
itemArr.slice(-1)[0].push(station.name);
} else {
indexList.push(char);
itemArr.push([station.name]);
}
}
return { indexList, itemArr };
},
},
onLoad(query) {
const { key } = query;
this.key = key;
},
methods: {
handleSelectedStation(stationName) {
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
prevPage.$vm[this.key] = stationName;
uni.navigateBack({ delta: 1 });
},
},
};
</script>

@ -0,0 +1,87 @@
<template>
<view v-if="nextTicket" class="page-box TrainItem lg">
<view class="px-base py-llg">
<view>
<u-row :gutter="12">
<u-col :span="4">
<view class="Time">{{
nextTicket.fromTime | timeFormat("HH:mm")
}}</view>
<view class="Station">{{ nextTicket.from }}</view>
</u-col>
<u-col :span="4">
<view class="text-center">
<view class="my-ssm">{{ nextTicket.trainNo }}</view>
<u-line></u-line>
<view
class="my-ssm text-center text-info"
:style="{ fontSize: '15px' }"
>{{ $u.dayjs(nextTicket.date).format("MM月DD日") }}&ensp;{{
$u.dayjs(nextTicket.date).isToday()
? "今天"
: $u.timeFormat(nextTicket.date, "[周]dd")
}}</view
>
</view>
</u-col>
<u-col :span="4">
<view class="Time text-end">{{
nextTicket.toTime | timeFormat("HH:mm")
}}</view>
<view class="Station text-end">{{ nextTicket.to }}</view>
</u-col>
</u-row>
</view>
</view>
</view>
<view v-else-if="loading" class="u-flex-xy-center p-base">
<text class="text-info">加载中...</text>
</view>
<view v-else class="u-flex-xy-center p-base">
<text class="text-info">当前没有出行计划</text>
</view>
</template>
<script>
import { mapState } from "vuex";
import apiTickets from "@/api/tickets";
import { timeAscCompareFn } from "@/utils/functions.js";
export default {
data() {
return {
loading: false,
nextTicket: undefined,
};
},
computed: {
...mapState({ auth: "auth" }),
},
mounted() {
this.loadMyTickets();
},
methods: {
loadMyTickets() {
this.loading = true;
apiTickets
.get()
.then((res) => {
this.nextTicket = res.data
.sort(
(a, b) =>
timeAscCompareFn(a.fromTime, b.fromTime) ||
timeAscCompareFn(a.toTime, b.toTime) ||
a.id - b.id
)
.find((tkt) => uni.$u.dayjs().isBefore(uni.$u.dayjs(tkt.toTime)));
})
.catch(() => {
this.nextTicket = undefined;
})
.then(() => {
this.loading = false;
});
},
},
};
</script>

@ -0,0 +1,249 @@
<template>
<view class="user-view p-page">
<u-navbar :placeholder="true" :bg-color="uNavbarBgColor" left-icon="">
<view slot="left">
<text
class="fw-bold"
:style="{ color: uNavbarTitleColor, fontSize: '22px' }"
>我的</text
>
</view>
<view slot="right" class="u-flex">
<u-icon
name="setting"
:size="30"
:color="uNavbarIconColor"
class="mx-sm"
></u-icon>
<u-icon
name="question-circle"
:size="30"
:color="uNavbarIconColor"
class="mx-sm"
></u-icon>
</view>
</u-navbar>
<view class="mt-base u-flex-y-center">
<u-avatar :size="65" @tap="handleToLogin"></u-avatar>
<view class="ms-base">
<view @tap="handleToLogin">
<text class="text-white fw-bold" :style="{ fontSize: '24px' }">{{
auth.name || "未登录"
}}</text>
</view>
<view class="mt-base">
<text class="text-white">便捷出行就在12306</text>
</view>
</view>
</view>
<view class="mt-llg page-box">
<view class="pt-base px-base">
<u-text text="当前出行" :size="18" :bold="true"> TODO </u-text>
</view>
<NextTicketVue ref="NextTicketVue" v-if="auth.id"></NextTicketVue>
<view v-else class="py-llg u-flex-xy-center">
<view>
<u-button type="primary" size="small" @tap="handleToLogin"
>去登录</u-button
>
</view>
</view>
</view>
<view v-if="auth.id" class="mt-base page-box">
<u-cell-group>
<u-cell title="身份证号" :label="auth.idCardNo | hideIdCardNo"></u-cell>
<u-cell title="手机号" :label="auth.mobileNo | hideMobileNo"></u-cell>
<u-cell
title="银行卡号"
:label="auth.bankCardNo | hideBankCardNo"
></u-cell>
</u-cell-group>
</view>
<!--
<view class="mt-base page-box u-flex-y-center">
<view class="u-flex-grow grid-item">
<u-image
src="@/static/img_chengcheren.png"
height="36px"
width="36px"
></u-image>
<view class="grid-item-text">乘车人</view>
</view>
<u-line direction="col" length="26px"></u-line>
<view class="u-flex-grow grid-item">
<u-image
src="@/static/img_dingdan.png"
height="36px"
width="36px"
></u-image>
<view class="grid-item-text">我的订单</view>
</view>
<u-line direction="col" length="26px"></u-line>
<view class="u-flex-grow grid-item">
<u-image
src="@/static/img_youhuiquan.png"
height="36px"
width="36px"
></u-image>
<view class="grid-item-text">优惠券</view>
</view>
</view>
-->
<!--
<view class="page-box">
<view class="px-base py-lg">
<u-text text="出行向导" :size="18" :bold="true"></u-text>
</view>
<u-line></u-line>
<u-grid :border="false" :col="4">
<u-grid-item v-for="item in PAGE_MENU_1" :key="item.label">
<view class="grid-item">
<u-image :src="item.image" height="30px" width="30px"></u-image>
<view class="grid-item-text">{{ item.label }}</view>
</view>
</u-grid-item>
</u-grid>
</view>
-->
<!--
<view class="page-box">
<view class="px-base py-lg">
<u-text text="温馨服务" :size="18" :bold="true"></u-text>
</view>
<u-line></u-line>
<u-grid :border="false" :col="4">
<u-grid-item v-for="item in PAGE_MENU_2" :key="item.label">
<view class="grid-item">
<u-image :src="item.image" height="30px" width="30px"></u-image>
<view class="grid-item-text">{{ item.label }}</view>
</view>
</u-grid-item>
</u-grid>
</view>
-->
<!--
<view class="page-box">
<view class="px-base py-lg">
<u-text text="信息服务" :size="18" :bold="true"></u-text>
</view>
<u-cell-group>
<u-cell title="公告" size="large" :is-link="true"></u-cell>
<u-cell title="常见问题" size="large" :is-link="true"></u-cell>
<u-cell title="使用须知" size="large" :is-link="true"></u-cell>
<u-cell title="服务规章" size="large" :is-link="true"></u-cell>
<u-cell title="铁路保险" size="large" :is-link="true"></u-cell>
<u-cell
title="关于"
size="large"
:is-link="true"
:border="false"
></u-cell>
</u-cell-group>
</view>
-->
<view :style="{ height: '25vh' }"></view>
<view v-if="auth.id" class="mt-base">
<u-button type="error" @tap="handleLogout">退</u-button>
</view>
</view>
</template>
<script>
import { mapActions, mapState } from "vuex";
import uNavbarOpacity from "@/utils/mixins/u-navbar-opacity";
import NextTicketVue from "./NextTicket.vue";
const PAGE_MENU_1 = [
{ label: "车站大屏", image: require("@/static/icon_chezhandaping.png") },
{ label: "时刻表", image: require("@/static/icon_shikebiao.png") },
{ label: "起售时间", image: require("@/static/icon_qishoushijian.png") },
{
label: "正晚点查询",
image: require("@/static/icon_zhengwandianchaxun.png"),
},
{ label: "票价查询", image: require("@/static/icon_piaojiachaxun.png") },
{ label: "换乘时间", image: require("@/static/icon_huanchengshijian.png") },
{
label: "代售点查询",
image: require("@/static/icon_daishoudianchaxun.png"),
},
{ label: "更多", image: require("@/static/icon_gengduo.png") },
];
const PAGE_MENU_2 = [
{
label: "临时身份证明",
image: require("@/static/icon_linshishenfenzhengming.png"),
},
{ label: "遗失物品", image: require("@/static/icon_yishiwupin.png") },
{ label: "建议", image: require("@/static/icon_jianyi.png") },
{ label: "投诉", image: require("@/static/icon_tousu.png") },
{ label: "重点旅客", image: require("@/static/icon_zhongdianlvke.png") },
{ label: "客服电话", image: require("@/static/icon_kefudianhua.png") },
{ label: "希望工程", image: require("@/static/icon_xiwanggongcheng.png") },
{
label: "外国商务人员",
image: require("@/static/icon_waiguoshangwurenyuan.png"),
},
];
export default {
mixins: [uNavbarOpacity],
components: { NextTicketVue },
data() {
return {
PAGE_MENU_1,
PAGE_MENU_2,
};
},
computed: {
...mapState({ auth: "auth" }),
},
onShow() {
// onShow onShow 访 nextTicket
if (this.$refs.NextTicketVue) this.$refs.NextTicketVue.loadMyTickets();
},
methods: {
...mapActions({ revertAuth: "revertAuth" }),
handleToLogin() {
if (this.auth.id) return;
uni.navigateTo({ url: "/pages/LoginView/LoginView" });
},
handleLogout() {
this.revertAuth();
},
},
};
</script>
<style scoped>
.user-view {
background: linear-gradient(
var(--color-app-blue) 0 100px,
250px,
var(--color-background) 400px
);
}
.grid-item {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space-llg) var(--space-ssm);
}
.grid-item-text {
margin-top: var(--space-sm);
font-size: 13px;
white-space: nowrap;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 KiB

@ -0,0 +1,35 @@
import Vue from "vue";
import Vuex from "vuex";
import storeAuth from "./modules/auth";
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
stations: [],
},
mutations: {
SET_STATIONS(state, stations) {
state.stations = stations || [];
},
},
actions: {
/**
* [{ id: 1, name: "长沙南站" }]
*/
updateStations({ commit }, stations) {
commit(
"SET_STATIONS",
stations.sort((a, b) =>
a.pinyin < b.pinyin ? -1 : a.pinyin > b.pinyin ? 1 : 0
)
);
},
},
modules: {
auth: storeAuth,
},
});
export default store;

@ -0,0 +1,16 @@
import getDefaultAuth from "./state";
export function updateAuth({ commit }, auth) {
auth = auth ?? getDefaultAuth();
if ("token" in auth) uni.setStorageSync("auth-token", auth.token);
if ("id" in auth) commit("SET_AUTH_ID", auth.id);
if ("account" in auth) commit("SET_AUTH_ACCOUNT", auth.account);
if ("name" in auth) commit("SET_AUTH_NAME", auth.name);
if ("idCardNo" in auth) commit("SET_AUTH_ID_CARD_NO", auth.idCardNo);
if ("mobileNo" in auth) commit("SET_AUTH_MOBILE_NO", auth.mobileNo);
if ("bankCardNo" in auth) commit("SET_AUTH_BANK_CARD_NO", auth.bankCardNo);
}
export function revertAuth({ dispatch }) {
dispatch("updateAuth", getDefaultAuth());
}

@ -0,0 +1,15 @@
/**
* Auth 代表登录用户信息
*/
import getDefaultAuth from "./state";
import * as mutations from "./mutations";
import * as actions from "./actions";
const storeAuth = {
state: getDefaultAuth(),
mutations,
actions,
};
export default storeAuth;

@ -0,0 +1,25 @@
import getDefaultAuth from "./state";
export function SET_AUTH_ID(state, id) {
state.id = id || getDefaultAuth().id;
}
export function SET_AUTH_ACCOUNT(state, account) {
state.account = account || getDefaultAuth().account;
}
export function SET_AUTH_NAME(state, name) {
state.name = name || getDefaultAuth().name;
}
export function SET_AUTH_ID_CARD_NO(state, idCardNo) {
state.idCardNo = idCardNo || getDefaultAuth().idCardNo;
}
export function SET_AUTH_MOBILE_NO(state, mobileNo) {
state.mobileNo = mobileNo || getDefaultAuth().mobileNo;
}
export function SET_AUTH_BANK_CARD_NO(state, bankCardNo) {
state.bankCardNo = bankCardNo || getDefaultAuth().bankCardNo;
}

@ -0,0 +1,12 @@
function getDefaultAuth() {
return {
id: undefined,
account: "", // 用户名
name: "", // 姓名
idCardNo: "", // 身份证号
mobileNo: undefined, // 手机号
bankCardNo: undefined, // 银行卡号
};
}
export default getDefaultAuth;

@ -0,0 +1,10 @@
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) => res[0] ? reject(res[0]) : resolve(res[1]));
});
},
});

@ -0,0 +1,118 @@
/**
* uni-app
*
* uni-app https://ext.dcloud.net.cn使
* 使scss使 import 便App
*
*/
/**
* App使
*
* 使scss scss 使 import
*/
@import "uview-ui/theme.scss";
/* 覆盖 theme.scs 的值 */
$u-bg-color: #f5f5f5;
/* 文字尺寸 */
$uni-font-size-sm: 14px;
$uni-font-size-base: 16px;
$uni-font-size-lg: 18px;
/* 间距 */
$space: 12px;
$spacers: (
ssm: calc(#{$space} / 3),
sm: calc(#{$space} / 2),
base: $space,
lg: calc(#{$space} / 3 * 4.5),
llg: calc(#{$space} / 3 * 6),
);
/* 颜色 */
$colors: (
primary: $u-primary,
primary-dark: $u-primary-dark,
primary-disabled: $u-primary-disabled,
primary-light: $u-primary-light,
warning: $u-warning,
warning-dark: $u-warning-dark,
warning-disabled: $u-warning-disabled,
warning-light: $u-warning-light,
success: $u-success,
success-dark: $u-success-dark,
success-disabled: $u-success-disabled,
success-light: $u-success-light,
error: $u-error,
error-dark: $u-error-dark,
error-disabled: $u-error-disabled,
error-light: $u-error-light,
info: $u-info,
info-dark: $u-info-dark,
info-disabled: $u-info-disabled,
info-light: $u-info-light,
main: $u-main-color,
content: $u-content-color,
tips: $u-tips-color,
light: $u-light-color,
white: #ffffff,
background: $u-bg-color,
app-blue: #238cfc, // rgb(35,140,252)
);
// /* */
// /* */
// $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-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-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;

@ -0,0 +1,46 @@
import dayjs from "dayjs";
require("dayjs/locale/zh-cn");
import isToday from "dayjs/plugin/isToday.js";
dayjs.locale("zh-cn");
dayjs.extend(isToday);
export { dayjs };
// https://dayjs.fenxianglu.cn/category/display.html
export function timeFormat(dateTime = null, formatStr = "YYYY-MM-DD") {
return dayjs(dateTime).format(formatStr);
}
/**
* 时间间隔返回值为 xx小时xx分钟
* @param {String} fromTime
* @param {String} toTime
*/
export function timeDiff(fromTime, toTime) {
const diff = dayjs(toTime).diff(dayjs(fromTime), "minute", true);
let result = "";
if (diff >= 60) {
result += `${parseInt(diff / 60)}小时`;
}
if (diff % 60 !== 0) {
result += `${diff % 60}`;
}
return result;
}
/**
* 隐藏身份证号
* @param {String} idCardNo
*/
export function hideIdCardNo(idCardNo) {
return idCardNo.replace(/(.{4}).+(.{3})/, `$1****$2`);
}
export function hideMobileNo(mobileNo) {
return mobileNo.replace(/(.{3}).+(.{4})/, `$1****$2`);
}
export function hideBankCardNo(bankCardNo) {
return bankCardNo.replace(/(.{4}).+(.{4})/, `$1****$2`);
}

@ -0,0 +1,58 @@
export function validateRequired(
rule,
value,
callback,
{ label, trim } = { label: "", trim: false }
) {
let val = String(value ?? "");
if (trim) val = val.trim();
if (val.length === 0) {
callback(new Error(`请输入${label}`));
} else {
callback();
}
}
export function validateAccount(rule, value, callback) {
const val = String(value ?? "");
if (!val) {
callback(new Error("请输入用户名"));
} else if (/[A-Za-z][A-Za-z0-9_]{5,29}/.test(val)) {
callback();
} else {
callback(
new Error(
`用户名只能填写字母、数字、下划线开头必须为字母且长度在6~30位内`
)
);
}
}
export function validatePassword(rule, value, callback) {
const val = String(value ?? "");
if (!val) {
callback(new Error("请输入密码"));
} else if (
/(?!^\d+$)(?!^[A-Za-z]+$)(?!^_+$)^\w{6,30}$/.test(String(value ?? ""))
) {
callback();
} else {
callback(
new Error(
`密码格式错误,必须且只能包含字母、数字、下划线中的两种或两种以上`
)
);
}
}
export function validatePasswordConfirm(rule, value, callback, password) {
const val = String(value ?? "");
if (!val) {
callback(new Error("请再次输入密码"));
} else if (val !== password) {
callback(new Error("两次输入密码不一致"));
} else {
callback();
}
}

@ -0,0 +1,8 @@
export function timeAscCompareFn(aTime, bTime) {
const at = uni.$u.dayjs(aTime);
const bt = uni.$u.dayjs(bTime);
if (at.isBefore(bt)) return -1;
else if (at.isAfter(bt)) return 1;
else return 0;
}

@ -0,0 +1,20 @@
export default {
data() {
return {
uNavbarBgColor: "transparent",
uNavbarTitleColor: "transparent",
uNavbarIconColor: "var(--color-white)",
};
},
onPageScroll(e) {
let per = e.scrollTop / 200;
if (per < 0) per = 0;
else if (per > 1) per = 1;
this.uNavbarBgColor = `rgba(245,245,245,${per})`;
this.uNavbarTitleColor = `rgba(48,49,51,${per})`;
this.uNavbarIconColor = `rgba(${255 - (255 - 96) * per},${
255 - (255 - 98) * per
},${255 - (255 - 102) * per})`;
},
};

@ -0,0 +1,64 @@
/**
* https://www.uviewui.com/js/http.html
*/
import store from "@/store";
module.exports = (vm) => {
// 初始化请求配置
uni.$u.http.setConfig((config) => {
/* config 为默认全局配置*/
config.baseURL = "http://py12306.learnerhub.net"; // api 根域名,本地开发大概就是 localhost(看你怎么启动的后端服务器)
return config;
});
// 请求拦截
uni.$u.http.interceptors.request.use(
(config) => {
const authToken = uni.getStorageSync("auth-token");
if (authToken) {
config.header.Authorization = `Bearer ${authToken}`;
}
return config;
},
(config) => {
return Promise.reject(config);
}
);
// 响应拦截,对响应成功做点什么 可使用 async await 做异步操作
uni.$u.http.interceptors.response.use(
(response) => {
const data = response.data || {};
if (data.code === 0) {
return Promise.resolve(data);
} else {
if ([110001, 110003, 110005].includes(data.code)) {
// 以上 code 都是用户 auth 有问题,清空一下 store 数据
store.dispatch("revertAuth");
}
uni.hideLoading();
uni.showToast({
icon: "error",
duration: 2000,
title: data.msg || response.msg,
});
return Promise.reject(response);
}
},
(response) => {
uni.hideLoading();
// 对响应错误做点什么 statusCode !== 200
const data = response.data || {};
uni.showToast({
icon: "error",
duration: 2000,
title: data.msg || response.msg,
});
return Promise.reject(response);
}
);
};

@ -0,0 +1,3 @@
module.exports = {
transpileDependencies: ["uview-ui"],
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save